From 1fe49cbdf238c5d0de0a12db67a2302959ea45ff Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 10 Jun 2014 08:29:54 +1200 Subject: [PATCH 1/7] Implement 96 bit nonce/32 bit counter split for ChaCha. As described in http://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305 --- .../crypto/engines/ChaChaEngine.java | 50 +++++++++++-- .../crypto/engines/Salsa20Engine.java | 24 ++++--- .../crypto/engines/XSalsa20Engine.java | 7 +- .../crypto/tls/Chacha20Poly1305.java | 5 ++ .../bouncycastle/crypto/test/ChaChaTest.java | 72 +++++++++++++++++++ 5 files changed, 140 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaEngine.java b/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaEngine.java index d4c43897c2..849c4037e0 100644 --- a/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaEngine.java +++ b/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaEngine.java @@ -4,9 +4,22 @@ /** * Implementation of Daniel J. Bernstein's ChaCha stream cipher. + *

+ * ChaCha uses a 128 or 256 bit key and a 64 bit nonce. + *

+ * ChaCha as specified by DJB uses a 64 bit nonce and 64 bit counter. This implementation also + * supports the 96 bit nonce, 32 bit counter split described in draft-irtf-cfrg-chacha20-poly1305, and will select the split automatically based on the size + * of the provided nonce. */ public class ChaChaEngine extends Salsa20Engine { + /** Whether a long nonce/short counter split is being used */ + private boolean longNonce = false; + + // TODO: Extend 96bit nonce to Salsa20? + /** * Creates a 20 rounds ChaCha engine. */ @@ -51,7 +64,7 @@ protected void advanceCounter(long diff) protected void advanceCounter() { - if (++engineState[12] == 0) + if (++engineState[12] == 0 && !longNonce) { ++engineState[13]; } @@ -99,7 +112,7 @@ protected void retreatCounter() throw new IllegalStateException("attempt to reduce counter past zero."); } - if (--engineState[12] == -1) + if (--engineState[12] == -1 && !longNonce) { --engineState[13]; } @@ -107,12 +120,28 @@ protected void retreatCounter() protected long getCounter() { + if (longNonce) + { + return engineState[12]; + } return ((long)engineState[13] << 32) | (engineState[12] & 0xffffffffL); } protected void resetCounter() { - engineState[12] = engineState[13] = 0; + engineState[12] = 0; + if (!longNonce) + { + engineState[13] = 0; + } + } + + protected void validateNonce(byte[] ivBytes) + { + if (ivBytes.length != 8 && ivBytes.length != 12) + { + throw new IllegalArgumentException(getAlgorithmName() + " requires a 64/96 bit IV"); + } } protected void setKey(byte[] keyBytes, byte[] ivBytes) @@ -154,9 +183,17 @@ protected void setKey(byte[] keyBytes, byte[] ivBytes) engineState[3] = Pack.littleEndianToInt(constants, 12); } - // IV - engineState[14] = Pack.littleEndianToInt(ivBytes, 0); - engineState[15] = Pack.littleEndianToInt(ivBytes, 4); + // Nonce + longNonce = (ivBytes.length == 12); + + int nonceOffset = 0; + if (longNonce) + { + engineState[13] = Pack.littleEndianToInt(ivBytes, 0); + nonceOffset = 4; + } + engineState[14] = Pack.littleEndianToInt(ivBytes, nonceOffset); + engineState[15] = Pack.littleEndianToInt(ivBytes, nonceOffset + 4); } protected void generateKeyStream(byte[] output) @@ -180,6 +217,7 @@ public static void chachaCore(int rounds, int[] input, int[] x) { throw new IllegalArgumentException(); } + // TODO: Check if this is optimised to bitmask? if (rounds % 2 != 0) { throw new IllegalArgumentException("Number of rounds must be even"); diff --git a/core/src/main/java/org/bouncycastle/crypto/engines/Salsa20Engine.java b/core/src/main/java/org/bouncycastle/crypto/engines/Salsa20Engine.java index 13aada9e2e..2d8d204f17 100644 --- a/core/src/main/java/org/bouncycastle/crypto/engines/Salsa20Engine.java +++ b/core/src/main/java/org/bouncycastle/crypto/engines/Salsa20Engine.java @@ -12,6 +12,8 @@ /** * Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005 + *

+ * Salsa20 uses a 128 or 256 bit key and a 64 bit nonce. */ public class Salsa20Engine implements SkippingStreamCipher @@ -65,12 +67,11 @@ public Salsa20Engine(int rounds) } /** - * initialise a Salsa20 cipher. - * + * Initialise the cipher. + * * @param forEncryption whether or not we are for encryption. - * @param params the parameters required to set up the cipher. - * @exception IllegalArgumentException if the params argument is - * inappropriate. + * @param params a {@link ParametersWithIV} with a {@link KeyParameter} and nonce. + * @exception IllegalArgumentException if the params argument is inappropriate. */ public void init( boolean forEncryption, @@ -90,11 +91,11 @@ public void init( ParametersWithIV ivParams = (ParametersWithIV) params; byte[] iv = ivParams.getIV(); - if (iv == null || iv.length != getNonceSize()) + if (iv == null) { - throw new IllegalArgumentException(getAlgorithmName() + " requires exactly " + getNonceSize() - + " bytes of IV"); + throw new IllegalArgumentException(getAlgorithmName() + " requires an IV"); } + validateNonce(iv); CipherParameters keyParam = ivParams.getParameters(); if (keyParam == null) @@ -120,9 +121,12 @@ else if (keyParam instanceof KeyParameter) initialised = true; } - protected int getNonceSize() + protected void validateNonce(byte[] ivBytes) { - return 8; + if (ivBytes.length != 8) + { + throw new IllegalArgumentException(getAlgorithmName() + " requires a 64 bit IV"); + } } public String getAlgorithmName() diff --git a/core/src/main/java/org/bouncycastle/crypto/engines/XSalsa20Engine.java b/core/src/main/java/org/bouncycastle/crypto/engines/XSalsa20Engine.java index 45cc478e45..6062979826 100644 --- a/core/src/main/java/org/bouncycastle/crypto/engines/XSalsa20Engine.java +++ b/core/src/main/java/org/bouncycastle/crypto/engines/XSalsa20Engine.java @@ -14,9 +14,12 @@ public String getAlgorithmName() return "XSalsa20"; } - protected int getNonceSize() + protected void validateNonce(byte[] ivBytes) { - return 24; + if (ivBytes.length != 24) + { + throw new IllegalArgumentException(getAlgorithmName() + " requires a 192 bit IV"); + } } /** diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/Chacha20Poly1305.java b/core/src/main/java/org/bouncycastle/crypto/tls/Chacha20Poly1305.java index a82045bf4a..4ef8e327c6 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/Chacha20Poly1305.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/Chacha20Poly1305.java @@ -11,6 +11,11 @@ import org.bouncycastle.util.Arrays; import org.bouncycastle.util.Pack; +/** + * Implementation of the ChaCha20/Poly1305 AEAD construction described in draft-mavrogiannopoulos-chacha-tls using 96 bit nonce/32 bit counter split. + */ public class Chacha20Poly1305 implements TlsCipher { protected TlsContext context; diff --git a/core/src/test/java/org/bouncycastle/crypto/test/ChaChaTest.java b/core/src/test/java/org/bouncycastle/crypto/test/ChaChaTest.java index 45c063fec1..ff4e248a5a 100644 --- a/core/src/test/java/org/bouncycastle/crypto/test/ChaChaTest.java +++ b/core/src/test/java/org/bouncycastle/crypto/test/ChaChaTest.java @@ -1,8 +1,10 @@ package org.bouncycastle.crypto.test; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.SkippingStreamCipher; import org.bouncycastle.crypto.StreamCipher; import org.bouncycastle.crypto.engines.ChaChaEngine; import org.bouncycastle.crypto.params.KeyParameter; @@ -159,6 +161,57 @@ public void performTest() set6v1_0, set6v1_65472, set6v1_65536); reinitBug(); skipTest(); + + // ChaCha with 96 bit nonce vectors from + // http://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305#appendix-A.1 and + // http://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305#appendix-A.2 + + // ChaCha20 Block Function Vectors (96 bit nonce) + chacha_96_Test1("96nonceB1", 0, + new ParametersWithIV(new KeyParameter(Hex.decode("0000000000000000000000000000000000000000000000000000000000000000")), Hex.decode("000000000000000000000000")), zeroes, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"); + chacha_96_Test1("96nonceB2", 1, + new ParametersWithIV(new KeyParameter(Hex.decode("0000000000000000000000000000000000000000000000000000000000000000")), Hex.decode("000000000000000000000000")), zeroes, + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" + + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"); + chacha_96_Test1("96nonceB3", 1, + new ParametersWithIV(new KeyParameter(Hex.decode("0000000000000000000000000000000000000000000000000000000000000001")), Hex.decode("000000000000000000000000")), zeroes, + "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a" + + "8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0"); + chacha_96_Test1("96nonceB4", 2, + new ParametersWithIV(new KeyParameter(Hex.decode("00ff000000000000000000000000000000000000000000000000000000000000")), Hex.decode("000000000000000000000000")), zeroes, + "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca" + + "13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096"); + chacha_96_Test1("96nonceB5", 0, + new ParametersWithIV(new KeyParameter(Hex.decode("0000000000000000000000000000000000000000000000000000000000000000")), Hex.decode("000000000000000000000002")), zeroes, + "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c7" + + "8a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d"); + // ChaCha20 Encryption Test Vectors (96 bit nonce) + // ChaCha20 encryption vector 1 is just ChaCha block function test 1 + chacha_96_Test1("96noncev2", 1, + new ParametersWithIV(new KeyParameter(Hex + .decode("0000000000000000000000000000000000000000000000000000000000000001")), Hex + .decode("000000000000000000000002")), + "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), + "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250" + + "d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea8" + + "5ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e" + + "62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b0" + + "4b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a8" + + "6f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221"); + chacha_96_Test1("96noncev3",42, + new ParametersWithIV(new KeyParameter(Hex + .decode("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0")), Hex + .decode("000000000000000000000002")), + "'Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe." + .getBytes(StandardCharsets.US_ASCII), + "62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf" + + "166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553eb" + + "f39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f77" + + "04c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1"); + } private void chachaTest1(int rounds, CipherParameters params, String v0, String v192, String v256, String v448) @@ -243,6 +296,25 @@ private void chachaTest2(CipherParameters params, String v0, String v65472, Stri } } + private void chacha_96_Test1(String test, + int blockCounter, + CipherParameters params, + byte[] plaintext, + String expected) + { + SkippingStreamCipher chaCha = new ChaChaEngine(); + byte[] output = new byte[plaintext.length]; + + chaCha.init(true, params); + chaCha.seekTo(blockCounter * 64); + chaCha.processBytes(plaintext, 0, plaintext.length, output, 0); + + if (!areEqual(output, Hex.decode(expected))) + { + mismatch(test, expected, output); + } + } + private void mismatch(String name, String expected, byte[] found) { fail("mismatch on " + name, expected, new String(Hex.encode(found))); From 9b193f567392676611f286a59001da0395232610 Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 10:04:02 +1300 Subject: [PATCH 2/7] Minor typo --- .../main/java/org/bouncycastle/crypto/modes/EAXBlockCipher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bouncycastle/crypto/modes/EAXBlockCipher.java b/core/src/main/java/org/bouncycastle/crypto/modes/EAXBlockCipher.java index 209d5cdb17..2891eb9c91 100644 --- a/core/src/main/java/org/bouncycastle/crypto/modes/EAXBlockCipher.java +++ b/core/src/main/java/org/bouncycastle/crypto/modes/EAXBlockCipher.java @@ -193,7 +193,7 @@ public void processAADByte(byte in) { if (cipherInitialized) { - throw new IllegalStateException("AAD data cannot be added after encryption/decription processing has begun."); + throw new IllegalStateException("AAD data cannot be added after encryption/decryption processing has begun."); } mac.update(in); } From ad68c92f3cb4264546e013e5c4661ae62ba6b49e Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 10:04:19 +1300 Subject: [PATCH 3/7] Check input buffer size on Poly1305 update. --- .../src/main/java/org/bouncycastle/crypto/macs/Poly1305.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/bouncycastle/crypto/macs/Poly1305.java b/core/src/main/java/org/bouncycastle/crypto/macs/Poly1305.java index 7a346f1e81..38fd205fe1 100644 --- a/core/src/main/java/org/bouncycastle/crypto/macs/Poly1305.java +++ b/core/src/main/java/org/bouncycastle/crypto/macs/Poly1305.java @@ -176,6 +176,11 @@ public void update(final byte[] in, final int inOff, final int len) throws DataLengthException, IllegalStateException { + if (in.length < (inOff + len)) + { + throw new DataLengthException("Input buffer too short"); + } + int copied = 0; while (len > copied) { From c4fd24f8e94c3f53b2b8b903a21f703ca08b51cb Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 10:04:37 +1300 Subject: [PATCH 4/7] Javadoc for KeyParameter. --- .../crypto/params/KeyParameter.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/src/main/java/org/bouncycastle/crypto/params/KeyParameter.java b/core/src/main/java/org/bouncycastle/crypto/params/KeyParameter.java index 5c4fe0e0b1..5a3c812c79 100644 --- a/core/src/main/java/org/bouncycastle/crypto/params/KeyParameter.java +++ b/core/src/main/java/org/bouncycastle/crypto/params/KeyParameter.java @@ -2,17 +2,34 @@ import org.bouncycastle.crypto.CipherParameters; +/** + * A key parameter to a cipher. + */ public class KeyParameter implements CipherParameters { private byte[] key; + /** + * Construct a key parameter from key data.
+ * The key data is copied by this constructor. + * + * @param key the key data. + */ public KeyParameter( byte[] key) { this(key, 0, key.length); } + /** + * Construct a key parameter from key data.
+ * The key data is copied by this constructor. + * + * @param key the array containing the key data. + * @param keyOff the offset in the array where the key data begins. + * @param keyLen the length of the key data. + */ public KeyParameter( byte[] key, int keyOff, @@ -23,6 +40,9 @@ public KeyParameter( System.arraycopy(key, keyOff, this.key, 0, keyLen); } + /** + * Obtains a mutable reference to the key data in this parameter. + */ public byte[] getKey() { return key; From 8af8cfffe140aae067169d8db0ac84c475cf5c9e Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 10:31:40 +1300 Subject: [PATCH 5/7] Simplify raw Poly1305 test case. --- .../crypto/test/Poly1305Test.java | 75 +------------------ 1 file changed, 2 insertions(+), 73 deletions(-) diff --git a/core/src/test/java/org/bouncycastle/crypto/test/Poly1305Test.java b/core/src/test/java/org/bouncycastle/crypto/test/Poly1305Test.java index 08ee8e10a1..04271ca390 100644 --- a/core/src/test/java/org/bouncycastle/crypto/test/Poly1305Test.java +++ b/core/src/test/java/org/bouncycastle/crypto/test/Poly1305Test.java @@ -2,10 +2,7 @@ import java.security.SecureRandom; -import org.bouncycastle.crypto.BlockCipher; import org.bouncycastle.crypto.CipherKeyGenerator; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.KeyGenerationParameters; import org.bouncycastle.crypto.Mac; import org.bouncycastle.crypto.engines.AESFastEngine; @@ -24,51 +21,6 @@ public class Poly1305Test { private static final int MAXLEN = 1000; - private static class KeyEngine - implements BlockCipher - { - - private byte[] key; - private final int blockSize; - - public KeyEngine(int blockSize) - { - this.blockSize = blockSize; - } - - public void init(boolean forEncryption, CipherParameters params) - throws IllegalArgumentException - { - if (params instanceof KeyParameter) - { - this.key = ((KeyParameter)params).getKey(); - } - } - - public String getAlgorithmName() - { - return "Key"; - } - - public int getBlockSize() - { - return blockSize; - } - - public int processBlock(byte[] in, int inOff, byte[] out, int outOff) - throws DataLengthException, - IllegalStateException - { - System.arraycopy(key, 0, out, outOff, key.length); - return key.length; - } - - public void reset() - { - } - - } - private static class TestCase { private final byte[] key; @@ -167,8 +119,8 @@ private void testCase(int i) if (tc.nonce == null) { // Raw Poly1305 test - don't do any transform on AES key part - mac = new Poly1305(new KeyEngine(16)); - mac.init(new ParametersWithIV(new KeyParameter(tc.key), new byte[16])); + mac = new Poly1305(); + mac.init(new KeyParameter(tc.key)); } else { @@ -205,29 +157,6 @@ private void testSequential() mac.update(m, 0, len); mac.doFinal(out, 0); - // if (c == 678) - // { - // TestCase tc = CASES[0]; - // - // if (!Arrays.areEqual(tc.key, kr)) - // { - // System.err.println("Key bad"); - // System.err.println(new String(Hex.encode(tc.key))); - // System.err.println(new String(Hex.encode(kr))); - // System.exit(1); - // } - // if (!Arrays.areEqual(tc.nonce, n)) - // { - // System.err.println("Nonce bad"); - // System.exit(1); - // } - // System.out.printf("[%d] m: %s\n", c, new String(Hex.encode(m, 0, len))); - // System.out.printf("[%d] K: %s\n", c, new String(Hex.encodje(kr))); - // System.out.printf("[%d] N: %s\n", c, new String(Hex.encode(n))); - // System.out.printf("[%d] M: ", c); - // } - // System.out.printf("%d/%s\n", c, new String(Hex.encode(out))); - if (len >= MAXLEN) break; n[0] ^= loop; From ec9a990977ea1528ad7905811e3917f4092d39cd Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 10:33:07 +1300 Subject: [PATCH 6/7] Remove AEAD test util dependencies on underlying block cipher. --- .../org/bouncycastle/crypto/test/AEADTestUtil.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/src/test/java/org/bouncycastle/crypto/test/AEADTestUtil.java b/core/src/test/java/org/bouncycastle/crypto/test/AEADTestUtil.java index 5ffa72a1e6..9b62034bdb 100644 --- a/core/src/test/java/org/bouncycastle/crypto/test/AEADTestUtil.java +++ b/core/src/test/java/org/bouncycastle/crypto/test/AEADTestUtil.java @@ -14,6 +14,8 @@ public class AEADTestUtil { + private static final int MAX_PLAINTEXT = 5 * 1024; + public static void testTampering(Test test, AEADBlockCipher cipher, CipherParameters params) throws InvalidCipherTextException { @@ -194,9 +196,8 @@ public static void testOutputSizes(Test test, AEADBlockCipher cipher, AEADParame throws IllegalStateException, InvalidCipherTextException { - int maxPlaintext = cipher.getUnderlyingCipher().getBlockSize() * 10; - byte[] plaintext = new byte[maxPlaintext]; - byte[] ciphertext = new byte[maxPlaintext * 2]; + byte[] plaintext = new byte[MAX_PLAINTEXT]; + byte[] ciphertext = new byte[MAX_PLAINTEXT * 2]; // Check output size calculations for truncated ciphertext lengths cipher.init(true, params); @@ -282,10 +283,7 @@ public static void testBufferSizeChecks(Test test, AEADBlockCipher cipher, AEADP throws IllegalStateException, InvalidCipherTextException { - int blockSize = cipher.getUnderlyingCipher().getBlockSize(); - int maxPlaintext = (blockSize * 10); - byte[] plaintext = new byte[maxPlaintext]; - + byte[] plaintext = new byte[MAX_PLAINTEXT]; cipher.init(true, params); @@ -294,7 +292,7 @@ public static void testBufferSizeChecks(Test test, AEADBlockCipher cipher, AEADP try { - cipher.processBytes(new byte[maxPlaintext - 1], 0, maxPlaintext, new byte[expectedUpdateOutputSize], 0); + cipher.processBytes(new byte[plaintext.length - 1], 0, plaintext.length, new byte[expectedUpdateOutputSize], 0); fail(test, "processBytes should validate input buffer length"); } catch (DataLengthException e) From 030e1bb708c33d5f2c0d5273efc50384e198b770 Mon Sep 17 00:00:00 2001 From: Tim Whittington Date: Tue, 6 Jan 2015 11:03:07 +1300 Subject: [PATCH 7/7] Implementation of the ChaCha20/Poly1305 AEAD construction from draft-irtf-cfrg-chacha20-poly1305. Implementation supports any Salsa20 derivative in the AEAD construction, defaulting to ChaCha. --- .../crypto/engines/ChaChaPoly1305Engine.java | 378 ++++++++++++++++++ .../crypto/test/ChaChaPoly1305Test.java | 273 +++++++++++++ 2 files changed, 651 insertions(+) create mode 100644 core/src/main/java/org/bouncycastle/crypto/engines/ChaChaPoly1305Engine.java create mode 100644 core/src/test/java/org/bouncycastle/crypto/test/ChaChaPoly1305Test.java diff --git a/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaPoly1305Engine.java b/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaPoly1305Engine.java new file mode 100644 index 0000000000..c29d5b668c --- /dev/null +++ b/core/src/main/java/org/bouncycastle/crypto/engines/ChaChaPoly1305Engine.java @@ -0,0 +1,378 @@ +package org.bouncycastle.crypto.engines; + +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.OutputLengthException; +import org.bouncycastle.crypto.generators.Poly1305KeyGenerator; +import org.bouncycastle.crypto.macs.Poly1305; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.Pack; + +/** + * Implementation of the ChaCha20/Poly1305 AEAD construction described in + * draft-irtf-cfrg-chacha20-poly1305. + *

+ * This implementation can be used with {@link ChaChaEngine} or any other {@link Salsa20Engine} + * variant, and supports any nonce/counter split supported by the cipher engine (specifically it + * will not adapt shorter nonces to the 96 bit nonce length). + */ +public class ChaChaPoly1305Engine + implements AEADBlockCipher +{ + private static final int MAC_SIZE = 16; + private static final int BLOCK_SIZE = 64; + private static final byte[] ZEROES = new byte[16]; + + private final Salsa20Engine cipher; + private final Poly1305 mac = new Poly1305(); + private boolean forEncryption; + private byte[] initialAssociatedText; + + private boolean initialised; + private boolean cipherInitialized; + private int aadSize; + private int ctSize; + + private final byte[] bufBlock = new byte[BLOCK_SIZE + MAC_SIZE]; + private int bufOff; + private final byte[] macBlock = new byte[MAC_SIZE]; + + /** + * Default constructor, creates an authenticated cipher using ChaCha/20 with a Poly1305 MAC. + */ + public ChaChaPoly1305Engine() + { + this(new ChaChaEngine()); + } + + /** + * Constructs an authenticated cipher using a custom ChaCha (or any Salsa20 variant) cipher with + * a Poly1305 MAC. + * + * @param cipher the cipher engine to use. + */ + public ChaChaPoly1305Engine(Salsa20Engine cipher) + { + this.cipher = cipher; + } + + /** + * Initialises this cipher. + * + * @param forEncryption whether this engine is being initialised for encryption or decryption. + * @param params {@link ParametersWithIV} or {@link AEADParameters} specifying a key and nonce + * which will be passed directly to the cipher to initialise it. The mac size must be + * 128 if AEADParameters are used. + */ + public void init(boolean forEncryption, CipherParameters params) + throws IllegalArgumentException + { + this.forEncryption = forEncryption; + + byte[] nonce; + KeyParameter key; + if (params instanceof AEADParameters) + { + AEADParameters param = (AEADParameters)params; + + nonce = param.getNonce(); + initialAssociatedText = param.getAssociatedText(); + if (param.getMacSize() != (MAC_SIZE * 8)) + { + throw new IllegalArgumentException(getAlgorithmName() + " is only specified for 128 bit mac"); + } + key = param.getKey(); + } + else if (params instanceof ParametersWithIV) + { + ParametersWithIV param = (ParametersWithIV)params; + + nonce = param.getIV(); + initialAssociatedText = null; + if ((param.getParameters() != null) && !(param.getParameters() instanceof KeyParameter)) + { + throw new IllegalArgumentException(getAlgorithmName() + " needs a KeyParameter inside ParametersWithIV"); + } + key = (KeyParameter)param.getParameters(); + } + else + { + throw new IllegalArgumentException("invalid parameters passed to " + getAlgorithmName()); + } + + if (nonce == null) + { + throw new IllegalArgumentException("nonce must be specified."); + } + if ((key == null) && !initialised) + { + throw new IllegalStateException(getAlgorithmName() + + " KeyParameter can not be null for first initialisation"); + } + + cipher.init(true, new ParametersWithIV(key, nonce)); + + // poly1305_key_gen: Generate one time Poly1305 key using 'block' 0 of underlying cipher + byte[] firstBlock = new byte[BLOCK_SIZE]; + cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0); + + // poly1305_key_gen uses r = firstBlock[0..15] and s/k = firstBlock[16..31] + // The BC Poly1305 implementation expects 'r' after 'k', so shift 'r' block after 'k' + System.arraycopy(firstBlock, 0, firstBlock, 32, 16); + KeyParameter macKey = new KeyParameter(firstBlock, 16, 32); + Poly1305KeyGenerator.clamp(macKey.getKey()); + + mac.init(macKey); + + reset(); + initialised = true; + } + + public String getAlgorithmName() + { + return cipher.getAlgorithmName() + "-" + mac.getAlgorithmName(); + } + + public BlockCipher getUnderlyingCipher() + { + // TODO: drop this when an AEADCipher interface exists + throw new UnsupportedOperationException(); + } + + public void processAADByte(byte in) + { + if (cipherInitialized) + { + throw new IllegalStateException("AAD data cannot be added after encryption/decryption processing has begun."); + } + mac.update(in); + aadSize++; + } + + public void processAADBytes(byte[] in, int inOff, int len) + { + if (cipherInitialized) + { + throw new IllegalStateException("AAD data cannot be added after encryption/decryption processing has begun."); + } + mac.update(in, inOff, len); + aadSize += len; + } + + public int processByte(byte in, byte[] out, int outOff) + throws DataLengthException + { + if (out.length <= outOff) + { + throw new OutputLengthException("Output buffer is too short"); + } + + initCipher(); + + if (forEncryption) + { + byte enc = cipher.returnByte(in); + out[outOff] = enc; + mac.update(enc); + ctSize++; + return 1; + } + else + { + bufBlock[bufOff++] = in; + if (bufOff == bufBlock.length) + { + return decryptBlock(out, outOff); + } + return 0; + } + } + + private int decryptBlock(byte[] output, int offset) + { + int outputSize = bufBlock.length - MAC_SIZE; + + mac.update(bufBlock, 0, outputSize); + ctSize += outputSize; + cipher.processBytes(bufBlock, 0, outputSize, output, offset); + System.arraycopy(bufBlock, outputSize, bufBlock, 0, MAC_SIZE); + bufOff = MAC_SIZE; + return outputSize; + } + + public int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) + throws DataLengthException + { + initCipher(); + + if (forEncryption) + { + cipher.processBytes(in, inOff, len, out, outOff); + mac.update(out, outOff, len); + ctSize += len; + return len; + } + else + { + int resultLen = 0; + for (int i = 0; i < len; ++i) + { + bufBlock[bufOff++] = in[inOff + i]; + if (bufOff == bufBlock.length) + { + resultLen += decryptBlock(out, outOff + resultLen); + } + } + return resultLen; + } + } + + private void initCipher() + { + if (cipherInitialized) + { + return; + } + + // Pad AD to 16 bytes in MAC calculation + mac.update(ZEROES, 0, pad16(aadSize)); + + cipherInitialized = true; + } + + /** + * Calculates padding for a given input size to a 16 block boundary + */ + private static int pad16(int dataSize) + { + return (16 - (dataSize & 0x0F)) & 0x0F; + } + + public int doFinal(byte[] out, int outOff) + throws IllegalStateException, + InvalidCipherTextException + { + if (forEncryption) + { + if (out.length < (outOff + MAC_SIZE)) + { + throw new OutputLengthException("Output buffer too short"); + } + finaliseMac(out, outOff); + reset(false); + return MAC_SIZE; + } + else + { + int extra = bufOff; + if (extra < MAC_SIZE) + { + throw new InvalidCipherTextException("Data too short - expected tag in input data"); + } + extra -= MAC_SIZE; + if (extra > 0) + { + if (out.length < (outOff + extra)) + { + throw new OutputLengthException("Output buffer too short"); + } + mac.update(bufBlock, 0, extra); + ctSize += extra; + cipher.processBytes(bufBlock, 0, extra, out, outOff); + } + byte[] msgMac = new byte[MAC_SIZE]; + System.arraycopy(bufBlock, extra, msgMac, 0, MAC_SIZE); + + finaliseMac(macBlock, 0); + if (!Arrays.constantTimeAreEqual(macBlock, msgMac)) + { + throw new InvalidCipherTextException("mac check in " + getAlgorithmName() + " failed"); + } + reset(false); + return extra; + } + } + + private void finaliseMac(byte[] out, int outOff) + { + // Pad ciphertext to 16 bytes for MAC calculation + mac.update(ZEROES, 0, pad16(ctSize)); + Pack.longToLittleEndian(aadSize, bufBlock, 0); + Pack.longToLittleEndian(ctSize, bufBlock, 8); + mac.update(bufBlock, 0, 16); + mac.doFinal(out, outOff); + } + + public byte[] getMac() + { + return macBlock; + } + + public int getUpdateOutputSize(int len) + { + if (forEncryption) + { + return len; + } + else + { + int totalData = len + bufOff; + if (totalData < MAC_SIZE) + { + return 0; + } + totalData -= MAC_SIZE; + return totalData - (totalData % (bufBlock.length - MAC_SIZE)); + } + } + + public int getOutputSize(int len) + { + + if (forEncryption) + { + return len + MAC_SIZE; + } + else + { + int totalData = len + bufOff; + return totalData < MAC_SIZE ? 0 : totalData - MAC_SIZE; + } + } + + public void reset() + { + reset(true); + } + + private void reset(boolean clearMac) + { + // Set counter to 1 (skip first keystream block used in mac key gen) + cipherInitialized = false; + cipher.seekTo(BLOCK_SIZE); + mac.reset(); + + aadSize = 0; + if (initialAssociatedText != null) + { + processAADBytes(initialAssociatedText, 0, initialAssociatedText.length); + } + + ctSize = 0; + bufOff = 0; + Arrays.fill(bufBlock, (byte)0); + + if (clearMac) + { + Arrays.fill(macBlock, (byte)0); + } + } + +} diff --git a/core/src/test/java/org/bouncycastle/crypto/test/ChaChaPoly1305Test.java b/core/src/test/java/org/bouncycastle/crypto/test/ChaChaPoly1305Test.java new file mode 100644 index 0000000000..7496745e11 --- /dev/null +++ b/core/src/test/java/org/bouncycastle/crypto/test/ChaChaPoly1305Test.java @@ -0,0 +1,273 @@ +package org.bouncycastle.crypto.test; + +import java.security.SecureRandom; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.ChaChaPoly1305Engine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.util.Strings; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.test.SimpleTest; + +public class ChaChaPoly1305Test + extends SimpleTest +{ + // Test vector from https://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305-04#section-2.8.2 + private byte[] K1 = Hex.decode("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"); + private byte[] N1 = Hex.decode("070000004041424344454647"); + private byte[] A1 = Hex.decode("50515253c0c1c2c3c4c5c6c7"); + private byte[] P1 = Hex.decode("4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e"); + private byte[] C1 = Hex.decode("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b6116" + + "1ae10b594f09e26a7e902ecbd0600691"); + private byte[] T1 = Hex.decode("1ae10b594f09e26a7e902ecbd0600691"); + + // Additional vector from https://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305-04#appendix-A.5 + private byte[] K2 = Hex.decode("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0"); + private byte[] N2 = Hex.decode("000000000102030405060708"); + private byte[] A2 = Hex.decode("f33388860000000000004e91"); + private byte[] P2 = Hex.decode("496e7465726e65742d4472616674732061726520647261667420646f63756d65" + + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + + "726573732e2fe2809d"); + private byte[] C2 = Hex.decode("64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + + "a6ad5cb4022b02709b" + "eead9d67890cbb22392336fea1851f38"); + private byte[] T2 = Hex.decode("eead9d67890cbb22392336fea1851f38"); + + + private static final int NONCE_LEN = 12; + private static final int MAC_LEN = 16; + private static final int AUTHEN_LEN = 20; + + public String getName() + { + return "ChaChaPoly1305"; + } + + public void performTest() + throws Exception + { + checkVectors(1, K1, 128, N1, A1, P1, T1, C1); + checkVectors(2, K2, 128, N2, A2, P2, T2, C2); + + ivParamTest(1, new ChaChaPoly1305Engine(), K1, N1); + + randomTests(); + AEADTestUtil.testReset(this, new ChaChaPoly1305Engine(), new ChaChaPoly1305Engine(), new AEADParameters(new KeyParameter(K1), 128, N1)); + AEADTestUtil.testTampering(this, new ChaChaPoly1305Engine(), new AEADParameters(new KeyParameter(K1), 128, N1)); + AEADTestUtil.testOutputSizes(this, new ChaChaPoly1305Engine(), new AEADParameters(new KeyParameter(K1), 128, N1)); + AEADTestUtil.testBufferSizeChecks(this, new ChaChaPoly1305Engine(), new AEADParameters(new KeyParameter(K1), 128, N1)); + } + + private void checkVectors( + int count, + byte[] k, + int macSize, + byte[] n, + byte[] a, + byte[] p, + byte[] t, + byte[] c) + throws InvalidCipherTextException + { + byte[] fa = new byte[a.length / 2]; + byte[] la = new byte[a.length - (a.length / 2)]; + System.arraycopy(a, 0, fa, 0, fa.length); + System.arraycopy(a, fa.length, la, 0, la.length); + + checkVectors(count, "all initial associated data", k, macSize, n, a, null, p, t, c); + checkVectors(count, "subsequent associated data", k, macSize, n, null, a, p, t, c); + checkVectors(count, "split associated data", k, macSize, n, fa, la, p, t, c); + } + + private void checkVectors( + int count, + String additionalDataType, + byte[] k, + int macSize, + byte[] n, + byte[] a, + byte[] sa, + byte[] p, + byte[] t, + byte[] c) + throws InvalidCipherTextException + { + ChaChaPoly1305Engine enc = new ChaChaPoly1305Engine(); + ChaChaPoly1305Engine dec = new ChaChaPoly1305Engine(); + + AEADParameters parameters = new AEADParameters(new KeyParameter(k), macSize, n, a); + enc.init(true, parameters); + dec.init(false, parameters); + + runCheckVectors(count, enc, dec, additionalDataType, sa, p, t, c); + runCheckVectors(count, enc, dec, additionalDataType, sa, p, t, c); + + // key reuse test + parameters = new AEADParameters(null, macSize, n, a); + enc.init(true, parameters); + dec.init(false, parameters); + + runCheckVectors(count, enc, dec, additionalDataType, sa, p, t, c); + runCheckVectors(count, enc, dec, additionalDataType, sa, p, t, c); + } + + private void runCheckVectors( + int count, + ChaChaPoly1305Engine encCipher, + ChaChaPoly1305Engine decCipher, + String additionalDataType, + byte[] sa, + byte[] p, + byte[] t, + byte[] c) + throws InvalidCipherTextException + { + byte[] enc = new byte[encCipher.getOutputSize(p.length)]; + + if (sa != null) + { + encCipher.processAADBytes(sa, 0, sa.length); + } + + int len = encCipher.processBytes(p, 0, p.length, enc, 0); + + len += encCipher.doFinal(enc, len); + + if (!areEqual(c, enc)) + { + fail("encrypted stream fails to match in test " + count + " with " + additionalDataType, + new String(Hex.encode(c)), new String(Hex.encode(enc))); + } + + byte[] tmp = new byte[enc.length]; + + if (sa != null) + { + decCipher.processAADBytes(sa, 0, sa.length); + } + + len = decCipher.processBytes(enc, 0, enc.length, tmp, 0); + + len += decCipher.doFinal(tmp, len); + + byte[] dec = new byte[len]; + + System.arraycopy(tmp, 0, dec, 0, len); + + if (!areEqual(p, dec)) + { + fail("decrypted stream fails to match in test " + count + " with " + additionalDataType); + } + + if (!areEqual(t, decCipher.getMac())) + { + fail("MAC fails to match in test " + count + " with " + additionalDataType); + } + } + + private void ivParamTest( + int count, + AEADBlockCipher c, + byte[] k, + byte[] n) + throws InvalidCipherTextException + { + byte[] p = Strings.toByteArray("hello world!!"); + + c.init(true, new ParametersWithIV(new KeyParameter(k), n)); + + byte[] enc = new byte[c.getOutputSize(p.length)]; + + int len = c.processBytes(p, 0, p.length, enc, 0); + + len += c.doFinal(enc, len); + + c.init(false, new ParametersWithIV(new KeyParameter(k), n)); + + byte[] tmp = new byte[enc.length]; + + len = c.processBytes(enc, 0, enc.length, tmp, 0); + + len += c.doFinal(tmp, len); + + byte[] dec = new byte[len]; + + System.arraycopy(tmp, 0, dec, 0, len); + + if (!areEqual(p, dec)) + { + fail("decrypted stream fails to match in test " + count); + } + } + + private void randomTests() + throws InvalidCipherTextException + { + SecureRandom srng = new SecureRandom(); + for (int i = 0; i < 10; ++i) + { + randomTest(srng); + } + } + + private void randomTest( + SecureRandom srng) + throws InvalidCipherTextException + { + int DAT_LEN = srng.nextInt() >>> 22; // Note: JDK1.0 compatibility + byte[] nonce = new byte[NONCE_LEN]; + byte[] authen = new byte[AUTHEN_LEN]; + byte[] datIn = new byte[DAT_LEN]; + byte[] key = new byte[32]; + srng.nextBytes(nonce); + srng.nextBytes(authen); + srng.nextBytes(datIn); + srng.nextBytes(key); + + KeyParameter sessKey = new KeyParameter(key); + ChaChaPoly1305Engine c = new ChaChaPoly1305Engine(); + + AEADParameters params = new AEADParameters(sessKey, MAC_LEN * 8, nonce, authen); + c.init(true, params); + + byte[] intrDat = new byte[c.getOutputSize(datIn.length)]; + int outOff = c.processBytes(datIn, 0, DAT_LEN, intrDat, 0); + outOff += c.doFinal(intrDat, outOff); + + c.init(false, params); + byte[] datOut = new byte[c.getOutputSize(outOff)]; + int resultLen = c.processBytes(intrDat, 0, outOff, datOut, 0); + c.doFinal(datOut, resultLen); + + if (!areEqual(datIn, datOut)) + { + fail("ChaChaPoly1305 roundtrip failed to match"); + } + } + + public static void main(String[] args) + { + runTest(new ChaChaPoly1305Test()); + } +}