diff --git a/README.md b/README.md index 6a70af8689e5896096709437a2a2bd8cf0627825..f596889db06e4813168372cadd6dc35c719be4a5 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Tink is maintained by: ## Contact and mailing list -If you want to contribute, please read [CONTRIBUTING](https://github.com/google/tink/blob/master/CONTRIBUTING.md) and send us pull requests. +If you want to contribute, please read CONTRIBUTING and send us pull requests. You can also report bugs or request new tests. If you'd like to talk to our developers or get notified about major new tests, diff --git a/java/BUILD b/java/BUILD index 9bca1b8b2c5ba863987c66f3cd5ccdc53f8d3d53..ee6553c1eea9017ec82c2b4d5e9f5a2a839662ed 100644 --- a/java/BUILD +++ b/java/BUILD @@ -72,6 +72,7 @@ java_library( "//java/src/main/java/com/google/cloud/crypto/tink/subtle", "//java/src/main/java/com/google/cloud/crypto/tink/subtle:aead", "//java/src/main/java/com/google/cloud/crypto/tink/subtle:aead_envelope", + "//java/src/main/java/com/google/cloud/crypto/tink/subtle:chacha20", "//java/src/main/java/com/google/cloud/crypto/tink/subtle:ed25519", "//java/src/main/java/com/google/cloud/crypto/tink/subtle:gcp_credential", "//java/src/main/java/com/google/cloud/crypto/tink/subtle:hybrid", diff --git a/java/src/main/java/com/google/cloud/crypto/tink/subtle/BUILD b/java/src/main/java/com/google/cloud/crypto/tink/subtle/BUILD index 1e10855af94e77d1aa3d88148d6c57c79bfd0318..a30b2d59a3ed651fad0b0ae90c2c15fc0922e337 100644 --- a/java/src/main/java/com/google/cloud/crypto/tink/subtle/BUILD +++ b/java/src/main/java/com/google/cloud/crypto/tink/subtle/BUILD @@ -179,3 +179,16 @@ java_binary( ":x25519", ], ) + +# ChaCha20-Poly1305 subtle +java_library( + name = "chacha20", + srcs = [ + "ChaCha20.java", + "IndCpaCipher.java", + ], + javacopts = JAVACOPTS, + deps = [ + ":subtle", + ], +) diff --git a/java/src/main/java/com/google/cloud/crypto/tink/subtle/ChaCha20.java b/java/src/main/java/com/google/cloud/crypto/tink/subtle/ChaCha20.java new file mode 100644 index 0000000000000000000000000000000000000000..b9aac079a1a1ced19b707f116dde4b76a97f0b01 --- /dev/null +++ b/java/src/main/java/com/google/cloud/crypto/tink/subtle/ChaCha20.java @@ -0,0 +1,143 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.cloud.crypto.tink.subtle; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * DJB's ChaCha20 stream cipher based on RFC7539. + * https://tools.ietf.org/html/rfc7539 + */ +public class ChaCha20 implements IndCpaCipher { + + private static final int BLOCK_INT_SIZE = 16; + public static final int BLOCK_BYTE_SIZE = BLOCK_INT_SIZE * 4; + private static final int NONCE_INT_SIZE = 3; + public static final int NONCE_BYTE_SIZE = NONCE_INT_SIZE * 4; + private static final int KEY_INT_SIZE = 8; + public static final int KEY_BYTE_SIZE = KEY_INT_SIZE * 4; + + private static final int[] SIGMA = toIntArray(ByteBuffer.wrap( + new byte[]{'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k' })); + private static final int COUNTER_POS = SIGMA.length + KEY_INT_SIZE; + + // TODO(anergiz): change this to ImmutableByteArray. + private final byte[] key; + + /** + * Constructs a new ChaCha20 cipher with the supplied {@code key}. + * + * @throws IllegalArgumentException when {@code key} length is not {@link ChaCha20#KEY_BYTE_SIZE}. + */ + public ChaCha20(byte[] key) { + if (key.length != KEY_BYTE_SIZE) { + throw new IllegalArgumentException("The key length in bytes must be 32."); + } + this.key = key; + } + + private static int rotateLeft(int x, int y) { + return (x << y) | (x >>> -y); + } + + private static int[] toIntArray(ByteBuffer in) { + IntBuffer intBuffer = in.order(ByteOrder.LITTLE_ENDIAN).asIntBuffer(); + int[] ret = new int[intBuffer.remaining()]; + intBuffer.get(ret); + return ret; + } + + static void quarterRound(int[] x, int a, int b, int c, int d) { + x[a] += x[b]; x[d] = rotateLeft(x[d] ^ x[a], 16); + x[c] += x[d]; x[b] = rotateLeft(x[b] ^ x[c], 12); + x[a] += x[b]; x[d] = rotateLeft(x[d] ^ x[a], 8); + x[c] += x[d]; x[b] = rotateLeft(x[b] ^ x[c], 7); + } + + static void chaChaCore(ByteBuffer output, final int[] input) { + int[] x = Arrays.copyOf(input, input.length); + for (int i = 0; i < 10; i++) { + quarterRound(x, 0, 4, 8, 12); + quarterRound(x, 1, 5, 9, 13); + quarterRound(x, 2, 6, 10, 14); + quarterRound(x, 3, 7, 11, 15); + quarterRound(x, 0, 5, 10, 15); + quarterRound(x, 1, 6, 11, 12); + quarterRound(x, 2, 7, 8, 13); + quarterRound(x, 3, 4, 9, 14); + } + for (int i = 0; i < x.length; i++) { + x[i] += input[i]; + } + output.asIntBuffer().put(x); + } + + void update(ByteBuffer output, final byte[] input, int inPos, byte[] nonce, int counter) { + // Set the initial state based on https://tools.ietf.org/html/rfc7539#section-2.3 + int[] state = new int[BLOCK_INT_SIZE]; + int pos = 0; + System.arraycopy(SIGMA, 0, state, pos, SIGMA.length); + pos += SIGMA.length; + System.arraycopy(toIntArray(ByteBuffer.wrap(key)), 0, state, pos, KEY_INT_SIZE); + state[COUNTER_POS] = counter; + pos += KEY_INT_SIZE + 1; // additional one for counter + System.arraycopy(toIntArray(ByteBuffer.wrap(nonce)), 0, state, pos, NONCE_INT_SIZE); + + // Do the ChaCha20 operation on the input. + ByteBuffer buf = ByteBuffer.allocate(BLOCK_BYTE_SIZE).order(ByteOrder.LITTLE_ENDIAN); + pos = inPos; + int inLen = input.length - inPos; + int todo; + while (inLen > 0) { + todo = inLen < BLOCK_BYTE_SIZE ? inLen : BLOCK_BYTE_SIZE; + chaChaCore(buf, state); + for (int j = 0; j < todo; j++, pos++) { + output.put((byte) (input[pos] ^ buf.array()[j])); + } + inLen -= todo; + state[COUNTER_POS]++; + } + } + + @Override + public byte[] encrypt(final byte[] plaintext) throws GeneralSecurityException { + if (plaintext.length > Integer.MAX_VALUE - NONCE_BYTE_SIZE) { + throw new GeneralSecurityException("plaintext too long"); + } + byte[] nonce = Random.randBytes(NONCE_BYTE_SIZE); + ByteBuffer ciphertext = ByteBuffer.allocate(plaintext.length + NONCE_BYTE_SIZE); + ciphertext.put(nonce); + update(ciphertext, plaintext, 0, nonce, 1); + return ciphertext.array(); + } + + @Override + public byte[] decrypt(final byte[] ciphertext) throws GeneralSecurityException { + if (ciphertext.length < NONCE_BYTE_SIZE) { + throw new GeneralSecurityException("ciphertext too short"); + } + byte[] nonce = new byte[NONCE_BYTE_SIZE]; + System.arraycopy(ciphertext, 0, nonce, 0, NONCE_BYTE_SIZE); + ByteBuffer plaintext = ByteBuffer.allocate(ciphertext.length - NONCE_BYTE_SIZE); + update(plaintext, ciphertext, NONCE_BYTE_SIZE, nonce, 1); + return plaintext.array(); + } +} diff --git a/java/src/test/java/com/google/cloud/crypto/tink/subtle/ChaCha20Test.java b/java/src/test/java/com/google/cloud/crypto/tink/subtle/ChaCha20Test.java new file mode 100644 index 0000000000000000000000000000000000000000..542b3997a88ff06cf7439944b650582800e0cb5c --- /dev/null +++ b/java/src/test/java/com/google/cloud/crypto/tink/subtle/ChaCha20Test.java @@ -0,0 +1,467 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.cloud.crypto.tink.subtle; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.cloud.crypto.tink.TestUtil; +import com.google.common.truth.Truth; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for ChaCha20. + */ +@RunWith(JUnit4.class) +public class ChaCha20Test { + + private static int[] twosCompInt(long[] a) { + int[] ret = new int[a.length]; + for (int i = 0; i < a.length; i++) { + ret[i] = (int) (a[i] - (a[i] > Integer.MAX_VALUE ? (1L << 32) : 0)); + } + return ret; + } + + private static byte[] twosCompByte(int[] a) { + byte[] ret = new byte[a.length]; + for (int i = 0; i < a.length; i++) { + ret[i] = (byte) (a[i] - (a[i] > Byte.MAX_VALUE ? (1 << 8) : 0)); + } + return ret; + } + + /** + * https://tools.ietf.org/html/rfc7539#section-2.1.1 + */ + @Test + public void testQuarterRound() { + int[] x = twosCompInt(new long[]{0x11111111, 0x01020304, 0x9b8d6f43, 0x01234567}); + ChaCha20.quarterRound(x, 0, 1, 2, 3); + Truth.assertThat(x).isEqualTo( + twosCompInt(new long[]{0xea2a92f4, 0xcb1cf8ce, 0x4581472e, 0x5881c4bb})); + } + + /** + * https://tools.ietf.org/html/rfc7539#section-2.2.1 + */ + @Test + public void testQuarterRound16() { + int[] x = twosCompInt(new long[]{ + 0x879531e0, 0xc5ecf37d, 0x516461b1, 0xc9a62f8a, + 0x44c20ef3, 0x3390af7f, 0xd9fc690b, 0x2a5f714c, + 0x53372767, 0xb00a5631, 0x974c541a, 0x359e9963, + 0x5c971061, 0x3d631689, 0x2098d9d6, 0x91dbd320}); + ChaCha20.quarterRound(x, 2, 7, 8, 13); + Truth.assertThat(x).isEqualTo( + twosCompInt(new long[]{ + 0x879531e0, 0xc5ecf37d, 0xbdb886dc, 0xc9a62f8a, + 0x44c20ef3, 0x3390af7f, 0xd9fc690b, 0xcfacafd2, + 0xe46bea80, 0xb00a5631, 0x974c541a, 0x359e9963, + 0x5c971061, 0xccc07c79, 0x2098d9d6, 0x91dbd320})); + } + + /** + * https://tools.ietf.org/html/rfc7539#section-2.3.2 + */ + @Test + public void testChaCha20Core() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c, + 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, + 0x00000001, 0x09000000, 0x4a000000, 0x00000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0x10, 0xf1, 0xe7, 0xe4, 0xd1, 0x3b, 0x59, 0x15, + 0x50, 0x0f, 0xdd, 0x1f, 0xa3, 0x20, 0x71, 0xc4, + 0xc7, 0xd1, 0xf4, 0xc7, 0x33, 0xc0, 0x68, 0x03, + 0x04, 0x22, 0xaa, 0x9a, 0xc3, 0xd4, 0x6c, 0x4e, + 0xd2, 0x82, 0x64, 0x46, 0x07, 0x9f, 0xaa, 0x09, + 0x14, 0xc2, 0xd7, 0x05, 0xd9, 0x8b, 0x02, 0xa2, + 0xb5, 0x12, 0x9c, 0xd1, 0xde, 0x16, 0x4e, 0xb9, + 0xcb, 0xd0, 0x83, 0xe8, 0xa2, 0x50, 0x3c, 0x4e + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#section-2.4.2 + */ + @Test + public void testChaCha20() { + byte[] in = ("Ladies and Gentlemen of the class of '99: If I could offer you only one tip for " + + "the future, sunscreen would be it.").getBytes(StandardCharsets.US_ASCII); + byte[] key = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f}; + byte[] nonce = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00}; + ChaCha20 cipher = new ChaCha20(key); + ByteBuffer out = ByteBuffer.allocate(in.length); + cipher.update(out, in, 0, nonce, 1); + Truth.assertThat(out.array()).isEqualTo(twosCompByte(new int[]{ + 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, + 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, + 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, + 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b, + 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, + 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57, + 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, + 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8, + 0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, + 0x56, 0xa3, 0x8e, 0x08, 0x8a, 0x22, 0xb6, 0x5e, + 0x52, 0xbc, 0x51, 0x4d, 0x16, 0xcc, 0xf8, 0x06, + 0x81, 0x8c, 0xe9, 0x1a, 0xb7, 0x79, 0x37, 0x36, + 0x5a, 0xf9, 0x0b, 0xbf, 0x74, 0xa3, 0x5b, 0xe6, + 0xb4, 0x0b, 0x8e, 0xed, 0xf2, 0x78, 0x5e, 0x42, + 0x87, 0x4d})); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.1 + * Test Vector #1 + */ + @Test + public void testChaCha20Core1() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, + 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86, 0xbd, 0x28, + 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, + 0xa8, 0x36, 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7, + 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d, + 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, + 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c, + 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86 + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.1 + * Test Vector #2 + */ + @Test + public void testChaCha20Core2() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0x9f, 0x07, 0xe7, 0xbe, 0x55, 0x51, 0x38, 0x7a, + 0x98, 0xba, 0x97, 0x7c, 0x73, 0x2d, 0x08, 0x0d, + 0xcb, 0x0f, 0x29, 0xa0, 0x48, 0xe3, 0x65, 0x69, + 0x12, 0xc6, 0x53, 0x3e, 0x32, 0xee, 0x7a, 0xed, + 0x29, 0xb7, 0x21, 0x76, 0x9c, 0xe6, 0x4e, 0x43, + 0xd5, 0x71, 0x33, 0xb0, 0x74, 0xd8, 0x39, 0xd5, + 0x31, 0xed, 0x1f, 0x28, 0x51, 0x0a, 0xfb, 0x45, + 0xac, 0xe1, 0x0a, 0x1f, 0x4b, 0x79, 0x4d, 0x6f + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.1 + * Test Vector #3 + */ + @Test + public void testChaCha20Core3() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x01000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0x3a, 0xeb, 0x52, 0x24, 0xec, 0xf8, 0x49, 0x92, + 0x9b, 0x9d, 0x82, 0x8d, 0xb1, 0xce, 0xd4, 0xdd, + 0x83, 0x20, 0x25, 0xe8, 0x01, 0x8b, 0x81, 0x60, + 0xb8, 0x22, 0x84, 0xf3, 0xc9, 0x49, 0xaa, 0x5a, + 0x8e, 0xca, 0x00, 0xbb, 0xb4, 0xa7, 0x3b, 0xda, + 0xd1, 0x92, 0xb5, 0xc4, 0x2f, 0x73, 0xf2, 0xfd, + 0x4e, 0x27, 0x36, 0x44, 0xc8, 0xb3, 0x61, 0x25, + 0xa6, 0x4a, 0xdd, 0xeb, 0x00, 0x6c, 0x13, 0xa0 + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.1 + * Test Vector #4 + */ + @Test + public void testChaCha20Core4() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x0000ff00, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000002, 0x00000000, 0x00000000, 0x00000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0x72, 0xd5, 0x4d, 0xfb, 0xf1, 0x2e, 0xc4, 0x4b, + 0x36, 0x26, 0x92, 0xdf, 0x94, 0x13, 0x7f, 0x32, + 0x8f, 0xea, 0x8d, 0xa7, 0x39, 0x90, 0x26, 0x5e, + 0xc1, 0xbb, 0xbe, 0xa1, 0xae, 0x9a, 0xf0, 0xca, + 0x13, 0xb2, 0x5a, 0xa2, 0x6c, 0xb4, 0xa6, 0x48, + 0xcb, 0x9b, 0x9d, 0x1b, 0xe6, 0x5b, 0x2c, 0x09, + 0x24, 0xa6, 0x6c, 0x54, 0xd5, 0x45, 0xec, 0x1b, + 0x73, 0x74, 0xf4, 0x87, 0x2e, 0x99, 0xf0, 0x96 + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.1 + * Test Vector #5 + */ + @Test + public void testChaCha20Core5() { + int[] in = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x02000000}; + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN); + ChaCha20.chaChaCore(buf, in); + Truth.assertThat(buf.array()).isEqualTo(twosCompByte(new int[]{ + 0xc2, 0xc6, 0x4d, 0x37, 0x8c, 0xd5, 0x36, 0x37, + 0x4a, 0xe2, 0x04, 0xb9, 0xef, 0x93, 0x3f, 0xcd, + 0x1a, 0x8b, 0x22, 0x88, 0xb3, 0xdf, 0xa4, 0x96, + 0x72, 0xab, 0x76, 0x5b, 0x54, 0xee, 0x27, 0xc7, + 0x8a, 0x97, 0x0e, 0x0e, 0x95, 0x5c, 0x14, 0xf3, + 0xa8, 0x8e, 0x74, 0x1b, 0x97, 0xc2, 0x86, 0xf7, + 0x5f, 0x8f, 0xc2, 0x99, 0xe8, 0x14, 0x83, 0x62, + 0xfa, 0x19, 0x8a, 0x39, 0x53, 0x1b, 0xed, 0x6d + })); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.2 + * Test Vector #1 + */ + @Test + public void testChaCha201() { + byte[] in = new byte[64]; + byte[] key = new byte[32]; + byte[] nonce = new byte[12]; + ChaCha20 cipher = new ChaCha20(key); + ByteBuffer out = ByteBuffer.allocate(in.length); + cipher.update(out, in, 0, nonce, 0); + Truth.assertThat(out.array()).isEqualTo(twosCompByte(new int[]{ + 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, + 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86, 0xbd, 0x28, + 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, + 0xa8, 0x36, 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7, + 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d, + 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, + 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c, + 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86})); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.2 + * Test Vector #2 + */ + @Test + public void testChaCha202() { + byte[] in = ("Any submission to the IETF intended by the Contributor for publication as all or " + + "part of an IETF Internet-Draft or RFC and any statement made within the context of an " + + "IETF activity is considered an \"IETF Contribution\". Such statements include oral " + + "statements in IETF sessions, as well as written and electronic communications made at " + + "any time or place, which are addressed to") + .getBytes(StandardCharsets.US_ASCII); + byte[] key = new byte[32]; + key[31] = 1; + byte[] nonce = new byte[12]; + nonce[11] = 2; + ChaCha20 cipher = new ChaCha20(key); + ByteBuffer out = ByteBuffer.allocate(in.length); + cipher.update(out, in, 0, nonce, 1); + Truth.assertThat(out.array()).isEqualTo(twosCompByte(new int[]{ + 0xa3, 0xfb, 0xf0, 0x7d, 0xf3, 0xfa, 0x2f, 0xde, + 0x4f, 0x37, 0x6c, 0xa2, 0x3e, 0x82, 0x73, 0x70, + 0x41, 0x60, 0x5d, 0x9f, 0x4f, 0x4f, 0x57, 0xbd, + 0x8c, 0xff, 0x2c, 0x1d, 0x4b, 0x79, 0x55, 0xec, + 0x2a, 0x97, 0x94, 0x8b, 0xd3, 0x72, 0x29, 0x15, + 0xc8, 0xf3, 0xd3, 0x37, 0xf7, 0xd3, 0x70, 0x05, + 0x0e, 0x9e, 0x96, 0xd6, 0x47, 0xb7, 0xc3, 0x9f, + 0x56, 0xe0, 0x31, 0xca, 0x5e, 0xb6, 0x25, 0x0d, + 0x40, 0x42, 0xe0, 0x27, 0x85, 0xec, 0xec, 0xfa, + 0x4b, 0x4b, 0xb5, 0xe8, 0xea, 0xd0, 0x44, 0x0e, + 0x20, 0xb6, 0xe8, 0xdb, 0x09, 0xd8, 0x81, 0xa7, + 0xc6, 0x13, 0x2f, 0x42, 0x0e, 0x52, 0x79, 0x50, + 0x42, 0xbd, 0xfa, 0x77, 0x73, 0xd8, 0xa9, 0x05, + 0x14, 0x47, 0xb3, 0x29, 0x1c, 0xe1, 0x41, 0x1c, + 0x68, 0x04, 0x65, 0x55, 0x2a, 0xa6, 0xc4, 0x05, + 0xb7, 0x76, 0x4d, 0x5e, 0x87, 0xbe, 0xa8, 0x5a, + 0xd0, 0x0f, 0x84, 0x49, 0xed, 0x8f, 0x72, 0xd0, + 0xd6, 0x62, 0xab, 0x05, 0x26, 0x91, 0xca, 0x66, + 0x42, 0x4b, 0xc8, 0x6d, 0x2d, 0xf8, 0x0e, 0xa4, + 0x1f, 0x43, 0xab, 0xf9, 0x37, 0xd3, 0x25, 0x9d, + 0xc4, 0xb2, 0xd0, 0xdf, 0xb4, 0x8a, 0x6c, 0x91, + 0x39, 0xdd, 0xd7, 0xf7, 0x69, 0x66, 0xe9, 0x28, + 0xe6, 0x35, 0x55, 0x3b, 0xa7, 0x6c, 0x5c, 0x87, + 0x9d, 0x7b, 0x35, 0xd4, 0x9e, 0xb2, 0xe6, 0x2b, + 0x08, 0x71, 0xcd, 0xac, 0x63, 0x89, 0x39, 0xe2, + 0x5e, 0x8a, 0x1e, 0x0e, 0xf9, 0xd5, 0x28, 0x0f, + 0xa8, 0xca, 0x32, 0x8b, 0x35, 0x1c, 0x3c, 0x76, + 0x59, 0x89, 0xcb, 0xcf, 0x3d, 0xaa, 0x8b, 0x6c, + 0xcc, 0x3a, 0xaf, 0x9f, 0x39, 0x79, 0xc9, 0x2b, + 0x37, 0x20, 0xfc, 0x88, 0xdc, 0x95, 0xed, 0x84, + 0xa1, 0xbe, 0x05, 0x9c, 0x64, 0x99, 0xb9, 0xfd, + 0xa2, 0x36, 0xe7, 0xe8, 0x18, 0xb0, 0x4b, 0x0b, + 0xc3, 0x9c, 0x1e, 0x87, 0x6b, 0x19, 0x3b, 0xfe, + 0x55, 0x69, 0x75, 0x3f, 0x88, 0x12, 0x8c, 0xc0, + 0x8a, 0xaa, 0x9b, 0x63, 0xd1, 0xa1, 0x6f, 0x80, + 0xef, 0x25, 0x54, 0xd7, 0x18, 0x9c, 0x41, 0x1f, + 0x58, 0x69, 0xca, 0x52, 0xc5, 0xb8, 0x3f, 0xa3, + 0x6f, 0xf2, 0x16, 0xb9, 0xc1, 0xd3, 0x00, 0x62, + 0xbe, 0xbc, 0xfd, 0x2d, 0xc5, 0xbc, 0xe0, 0x91, + 0x19, 0x34, 0xfd, 0xa7, 0x9a, 0x86, 0xf6, 0xe6, + 0x98, 0xce, 0xd7, 0x59, 0xc3, 0xff, 0x9b, 0x64, + 0x77, 0x33, 0x8f, 0x3d, 0xa4, 0xf9, 0xcd, 0x85, + 0x14, 0xea, 0x99, 0x82, 0xcc, 0xaf, 0xb3, 0x41, + 0xb2, 0x38, 0x4d, 0xd9, 0x02, 0xf3, 0xd1, 0xab, + 0x7a, 0xc6, 0x1d, 0xd2, 0x9c, 0x6f, 0x21, 0xba, + 0x5b, 0x86, 0x2f, 0x37, 0x30, 0xe3, 0x7c, 0xfd, + 0xc4, 0xfd, 0x80, 0x6c, 0x22, 0xf2, 0x21})); + } + + /** + * https://tools.ietf.org/html/rfc7539#appendix-A.2 + * Test Vector #3 + */ + @Test + public void testChaCha203() { + byte[] in = twosCompByte(new int[]{ + 0x27, 0x54, 0x77, 0x61, 0x73, 0x20, 0x62, 0x72, + 0x69, 0x6c, 0x6c, 0x69, 0x67, 0x2c, 0x20, 0x61, + 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, + 0x6c, 0x69, 0x74, 0x68, 0x79, 0x20, 0x74, 0x6f, + 0x76, 0x65, 0x73, 0x0a, 0x44, 0x69, 0x64, 0x20, + 0x67, 0x79, 0x72, 0x65, 0x20, 0x61, 0x6e, 0x64, + 0x20, 0x67, 0x69, 0x6d, 0x62, 0x6c, 0x65, 0x20, + 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, + 0x61, 0x62, 0x65, 0x3a, 0x0a, 0x41, 0x6c, 0x6c, + 0x20, 0x6d, 0x69, 0x6d, 0x73, 0x79, 0x20, 0x77, + 0x65, 0x72, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x62, 0x6f, 0x72, 0x6f, 0x67, 0x6f, 0x76, 0x65, + 0x73, 0x2c, 0x0a, 0x41, 0x6e, 0x64, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x6d, 0x6f, 0x6d, 0x65, 0x20, + 0x72, 0x61, 0x74, 0x68, 0x73, 0x20, 0x6f, 0x75, + 0x74, 0x67, 0x72, 0x61, 0x62, 0x65, 0x2e + }); + byte[] key = twosCompByte(new int[]{ + 0x1c, 0x92, 0x40, 0xa5, 0xeb, 0x55, 0xd3, 0x8a, + 0xf3, 0x33, 0x88, 0x86, 0x04, 0xf6, 0xb5, 0xf0, + 0x47, 0x39, 0x17, 0xc1, 0x40, 0x2b, 0x80, 0x09, + 0x9d, 0xca, 0x5c, 0xbc, 0x20, 0x70, 0x75, 0xc0}); + byte[] nonce = new byte[12]; + nonce[11] = 2; + ChaCha20 cipher = new ChaCha20(key); + ByteBuffer out = ByteBuffer.allocate(in.length); + cipher.update(out, in, 0, nonce, 42); + Truth.assertThat(out.array()).isEqualTo(twosCompByte(new int[]{ + 0x62, 0xe6, 0x34, 0x7f, 0x95, 0xed, 0x87, 0xa4, + 0x5f, 0xfa, 0xe7, 0x42, 0x6f, 0x27, 0xa1, 0xdf, + 0x5f, 0xb6, 0x91, 0x10, 0x04, 0x4c, 0x0d, 0x73, + 0x11, 0x8e, 0xff, 0xa9, 0x5b, 0x01, 0xe5, 0xcf, + 0x16, 0x6d, 0x3d, 0xf2, 0xd7, 0x21, 0xca, 0xf9, + 0xb2, 0x1e, 0x5f, 0xb1, 0x4c, 0x61, 0x68, 0x71, + 0xfd, 0x84, 0xc5, 0x4f, 0x9d, 0x65, 0xb2, 0x83, + 0x19, 0x6c, 0x7f, 0xe4, 0xf6, 0x05, 0x53, 0xeb, + 0xf3, 0x9c, 0x64, 0x02, 0xc4, 0x22, 0x34, 0xe3, + 0x2a, 0x35, 0x6b, 0x3e, 0x76, 0x43, 0x12, 0xa6, + 0x1a, 0x55, 0x32, 0x05, 0x57, 0x16, 0xea, 0xd6, + 0x96, 0x25, 0x68, 0xf8, 0x7d, 0x3f, 0x3f, 0x77, + 0x04, 0xc6, 0xa8, 0xd1, 0xbc, 0xd1, 0xbf, 0x4d, + 0x50, 0xd6, 0x15, 0x4b, 0x6d, 0xa7, 0x31, 0xb1, + 0x87, 0xb5, 0x8d, 0xfd, 0x72, 0x8a, 0xfa, 0x36, + 0x75, 0x7a, 0x79, 0x7a, 0xc1, 0x88, 0xd1})); + } + + @Test + public void testRandomChaCha20() throws GeneralSecurityException { + for (int i = 0; i < 1000; i++) { + byte[] expectedInput = Random.randBytes(new java.util.Random().nextInt(300)); + byte[] key = Random.randBytes(32); + ChaCha20 cipher = new ChaCha20(key); + byte[] output = cipher.encrypt(expectedInput); + byte[] nonce = Arrays.copyOf(output, ChaCha20.NONCE_BYTE_SIZE); + byte[] actualInput = cipher.decrypt(output); + assertTrue( + String.format( + "\n\nMessage: %s\nKey: %s\nNonce: %s\nOutput: %s\nDecrypted Msg: %s\n", + TestUtil.hexEncode(expectedInput), + TestUtil.hexEncode(key), + TestUtil.hexEncode(nonce), + TestUtil.hexEncode(output), + TestUtil.hexEncode(actualInput)), + Arrays.equals(expectedInput, actualInput)); + } + } + + @Test + public void testChaCha20ThrowsIllegalArgExpWhenKeyLenIsLessThan32() { + try { + new ChaCha20(new byte[1]); + fail("Expected IllegalArgumentException."); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); + } + } + + @Test + public void testChaCha20ThrowsIllegalArgExpWhenKeyLenIsGreaterThan32() { + try { + new ChaCha20(new byte[33]); + fail("Expected IllegalArgumentException."); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); + } + } + + @Test + public void testChaCha20DecryptThrowsGeneralSecurityExpWhenCiphertextIsTooShort() { + ChaCha20 chaCha20 = new ChaCha20(new byte[32]); + try { + chaCha20.decrypt(new byte[2]); + fail("Expected GeneralSecurityException."); + } catch (GeneralSecurityException e) { + assertThat(e).hasMessageThat().containsMatch("ciphertext too short"); + } + } +}