Java 11的新加密算法ChaCha20-Poly1305

ChaCha20-Poly1305 介绍

Java 11新增加了加密算法ChaCha20-Poly1305。
ChaCha20-Poly1305相关的名词需要解释一下:
ChaCha20是一种流式对称加密算法。
Poly1305是一种带密码的消息摘要算法。
ChaCha20-Poly1305是一种流式对称加密算法。

说明:我并没有找到Java 11中的Poly1305算法API,于是按照RFC7539实现了一下,见下文代码,为了便于对照原文,有的private函数命名采用下划线分割小写字母方式,不太符合Java规范。

ChaCha20 和 ChaCha20-Poly1305 的区别

这两种算法原理上是一致的。区别如下:
ChaCha20 是流式加密,密文长度和原文长度是相同的。
ChaCha20-Poly1305 在 ChaCha20 的基础上增加了Poly1305 算法运算结果。
ChaCha20 有四个输入:

  • 明文 Plaintext
  • 秘钥 Key
  • 秘钥 Nonce
  • 轮 Initial Counter,整数
    ChaCha20-Poly1305 有三个输入,不需要“轮”,其他输入和ChaCha20 一样。
    测试用例
    Key = 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f.
    Nonce = (00:00:00:00:00:00:00:4a:00:00:00:00).
    Initial Counter = 1.
    Plaintext
  000  4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c  Ladies and Gentl
  016  65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73  emen of the clas
  032  73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63  s of '99: If I c
  048  6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f  ould offer you o
  064  6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20  nly one tip for
  080  74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73  the future, suns
  096  63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69  creen would be i
  112  74 2e                                            t.

ChaCha20 加密结果:

  000   6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81
  016   e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b
  032   f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57
  048   16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8
  064   07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e
  080   52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36
  096   5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42
  112   87 4d

ChaCha20-Poly1305 加密结果:

  000   6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81
  016   e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b
  032   f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57
  048   16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8
  064   07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e
  080   52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36
  096   5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42
  112   87 4d 81 db 63 fc b1 89 a0 31 21 ae 0a c7 2a 3f
  128   1f 36

我们可以看到两者加密的结果是一致的,除了ChaCha20-Poly1305 加密后面多16个字节内容。
ChaCha20 加密主要代码:

byte[] chacha20(byte[] message, byte[] key, byte[] nonce, int counter) {
    Key theKey = new SecretKeySpec(key, "ChaCha20");
    ChaCha20ParameterSpec spec = new ChaCha20ParameterSpec(nonce, counter);
    Cipher cipher = Cipher.getInstance("ChaCha20");
    cipher.init(Cipher.ENCRYPT_MODE, theKey, spec);
    return cipher.doFinal(message);
}

ChaCha20-Poly1305 加密主要代码:

byte[] chacha20Ploy1305(byte[] message, byte[] key, byte[] nonce) {
    Key theKey = new SecretKeySpec(key, "ChaCha20-Poly1305");
    AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
    Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305");
    cipher.init(Cipher.ENCRYPT_MODE, theKey, spec);
    return cipher.doFinal(message);
}

全部代码

import org.junit.Assert;
import org.junit.Test;
import javax.crypto.Cipher;
import javax.crypto.spec.ChaCha20ParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;

public class ChaCha20Poly1305Test {
    byte[] nonce = hex2bytes("00 00 00 00 00 00 00 4a 00 00 00 00");
    byte[] key = hex2bytes("00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f" +
            "10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f");
    byte[] message = hex2bytes("4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c" +
            "65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73" +
            "73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63" +
            "6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f" +
            "6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20" +
            "74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73" +
            "63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69" +
            "74 2e");

    @Test
    public void testChaCha20Poly1305() {
        byte[] encoded = chacha20Ploy1305Encrypt(message, key, nonce);
        byte[] expected = hex2bytes("6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81" +
                "e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b" +
                "f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57" +
                "16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8" +
                "07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e" +
                "52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36" +
                "5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42" +
                "87 4d 81 db 63 fc b1 89 a0 31 21 ae 0a c7 2a 3f" +
                "1f 36");
        Assert.assertArrayEquals(expected, encoded);
        byte[] origin = chacha20Ploy1305Decrypt(encoded, key, nonce);
        Assert.assertArrayEquals(origin, message);
    }

    @Test
    public void testChaCha20() {
        int counter = 1;
        byte[] encoded = chacha20Encrypt(message, key, nonce, counter);
        byte[] expected = hex2bytes("6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81" +
                "e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b" +
                "f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57" +
                "16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8" +
                "07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e" +
                "52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36" +
                "5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42" +
                "87 4d");
        Assert.assertArrayEquals(expected, encoded);
        byte[] origin = chacha20Decrypt(encoded, key, nonce, counter);
        Assert.assertArrayEquals(origin, message);
    }

    @Test
    public void testPoly1305() {
        Poly1305 poly1305 = new Poly1305();
        message = hex2bytes("43 72 79 70 74 6f 67 72 61 70 68 69 63 20 46 6f" +
                "72 75 6d 20 52 65 73 65 61 72 63 68 20 47 72 6f" +
                "75 70");
        key = hex2bytes("85 d6 be 78 57 55 6d 33 7f 44 52 fe 42 d5 06 a8" +
                "01 03 80 8a fb 0d b2 fd 4a bf f6 af 41 49 f5 1b");
        byte[] expected = hex2bytes("a8 06 1d c1 30 51 36 c6 c2 2b 8b af 0c 01 27 a9");
        byte[] hashValue = poly1305.ploy1305(message, key);
        Assert.assertArrayEquals(expected, hashValue);
    }

    public byte[] hex2bytes(String hex) {
        hex = hex.replaceAll("[: ]", "");
        byte[] result = new byte[hex.length() / 2];
        for (int i = 0; i < result.length; i++) {
            String hexByte = hex.substring(i * 2, i * 2 + 2);
            result[i] = (byte) Integer.parseInt(hexByte, 16);
        }
        return result;
    }

    public byte[] chacha20Encrypt(byte[] message, byte[] key, byte[] nonce, int counter) {
        return chacha20(message, key, nonce, counter, Cipher.ENCRYPT_MODE);
    }

    public byte[] chacha20Decrypt(byte[] message, byte[] key, byte[] nonce, int counter) {
        return chacha20(message, key, nonce, counter, Cipher.DECRYPT_MODE);
    }

    public byte[] chacha20Ploy1305Encrypt(byte[] message, byte[] key, byte[] nonce) {
        return chacha20Ploy1305(message, key, nonce, Cipher.ENCRYPT_MODE);
    }

    public byte[] chacha20Ploy1305Decrypt(byte[] message, byte[] key, byte[] nonce) {
        return chacha20Ploy1305(message, key, nonce, Cipher.DECRYPT_MODE);
    }

    private byte[] chacha20(byte[] message, byte[] key, byte[] nonce, int counter, int mode) {
        // nonce 长度必须为 12字节
        if (nonce == null || nonce.length != 12) {
            throw new IllegalArgumentException("nonce must be 12 bytes in length");
        }
        // 密钥的长度必须为256位,即32字节
        if (key == null || key.length != 32) {
            throw new IllegalArgumentException("key length must be 256 bits");
        }
        Key theKey = new SecretKeySpec(key, "ChaCha20");
        ChaCha20ParameterSpec spec = new ChaCha20ParameterSpec(nonce, counter);
        try {
            Cipher cipher = Cipher.getInstance("ChaCha20");
            cipher.init(mode, theKey, spec);
            return cipher.doFinal(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] chacha20Ploy1305(byte[] message, byte[] key, byte[] nonce, int mode) {
        // nonce 长度必须为 12字节
        if (nonce == null || nonce.length != 12) {
            throw new IllegalArgumentException("nonce must be 12 bytes in length");
        }
        // 密钥的长度必须为256位,即32字节
        if (key == null || key.length != 32) {
            throw new IllegalArgumentException("key length must be 256 bits");
        }
        Key theKey = new SecretKeySpec(key, "ChaCha20-Poly1305");
        AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
        try {
            Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305");
            cipher.init(mode, theKey, spec);
            return cipher.doFinal(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

class Poly1305 {
    public byte[] ploy1305(byte[] msg, byte[] key) {
        BigInteger r = le_bytes_to_num(key, 0, 16);
        r = clamp(r);
        BigInteger s = le_num(key, 16, 16);
        BigInteger a = BigInteger.ZERO;
        BigInteger p = BigInteger.ONE.shiftLeft(130).add(BigInteger.valueOf(-5));
        for (int i = 1; i <= ceilMod(msg.length, 16); i++) {
            int len = 16;
            if (i == ceilMod(msg.length, 16) && (msg.length % 16 != 0)) {
                len = msg.length % 16;
            }
            BigInteger n = le_bytes_to_num_pad(msg, (i - 1) * 16, len);
            a = a.add(n);
            a = r.multiply(a).mod(p);
        }
        a = a.add(s);
        return num_to_16_le_bytes(a);
    }

    public long ceilMod(long value, long mod) {
        long result = value % mod;
        if (result == 0) {
            return value / mod;
        } else {
            return value / mod + 1;
        }
    }

    public byte[] hex2bytes(String s) {
        s = s.replaceAll("[: ]", "");
        byte[] result = new byte[s.length() / 2];
        for (int i = 0; i < result.length; i++) {
            String hex = s.substring(i * 2, i * 2 + 2);
            result[i] = (byte) Integer.parseInt(hex, 16);
        }
        return result;
    }

    public long toUnsigned(byte b) {
        return b >= 0 ? b : (b + Byte.MAX_VALUE * 2L + 2);
    }

    private byte[] num_to_16_le_bytes(BigInteger v) {
        byte[] value = v.toByteArray();
        byte[] result = new byte[16];
        for (int i = 0; i < 16; i++) {
            result[i] = value[value.length - 1 - i];
        }
        return result;
    }

    private BigInteger le_bytes_to_num(byte[] b, int off, int len) {
        StringBuilder sb = new StringBuilder();
        for (int i = off + len - 1; i >= off; i--) {
            sb.append(String.format("%02x", toUnsigned(b[i])));
        }
        return new BigInteger(sb.toString(), 16);
    }

    private BigInteger le_bytes_to_num_pad(byte[] b, int off, int len) {
        StringBuilder sb = new StringBuilder();
        sb.append("01");
        for (int i = off + len - 1; i >= off; i--) {
            sb.append(String.format("%02x", toUnsigned(b[i])));
        }
        return new BigInteger(sb.toString(), 16);
    }

    private BigInteger le_num(byte[] b, int off, int len) {
        byte[] bs = new byte[len];
        for (int i = 0; i < len; i++) {
            bs[i] = b[len - 1 - i + off];
        }
        return new BigInteger(bs, 0, len);
    }

    private BigInteger clamp(BigInteger r) {
        BigInteger magicNumber = new BigInteger("0ffffffc0ffffffc0ffffffc0fffffff", 16);
        return r.and(magicNumber);
    }
}

相关文章

对称加密
密码学基础系列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354