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);
}
}