diff options
author | Peter Dettman <peter.dettman@bouncycastle.org> | 2019-09-09 21:01:14 +0700 |
---|---|---|
committer | Peter Dettman <peter.dettman@bouncycastle.org> | 2019-09-09 21:01:14 +0700 |
commit | b4b389a9e08375c78a25b63aca2468abe75e5428 (patch) | |
tree | ed2f1d614f0109c375f5b6dccfc6c005080af994 | |
parent | Tampering and reuse test cases for GCM (diff) | |
download | BouncyCastle.NET-ed25519-b4b389a9e08375c78a25b63aca2468abe75e5428.tar.xz |
Port ChaCha20Poly1305 from bc-java
-rw-r--r-- | crypto/BouncyCastle.Android.csproj | 2 | ||||
-rw-r--r-- | crypto/BouncyCastle.csproj | 2 | ||||
-rw-r--r-- | crypto/BouncyCastle.iOS.csproj | 2 | ||||
-rw-r--r-- | crypto/crypto.csproj | 15 | ||||
-rw-r--r-- | crypto/src/crypto/modes/ChaCha20Poly1305.cs | 551 | ||||
-rw-r--r-- | crypto/src/crypto/modes/IAeadBlockCipher.cs | 102 | ||||
-rw-r--r-- | crypto/src/crypto/modes/IAeadCipher.cs | 111 | ||||
-rw-r--r-- | crypto/test/UnitTests.csproj | 1 | ||||
-rw-r--r-- | crypto/test/src/crypto/test/ChaCha20Poly1305Test.cs | 443 | ||||
-rw-r--r-- | crypto/test/src/crypto/test/RegressionTest.cs | 1 |
10 files changed, 1134 insertions, 96 deletions
diff --git a/crypto/BouncyCastle.Android.csproj b/crypto/BouncyCastle.Android.csproj index e20fa2b63..eeefee157 100644 --- a/crypto/BouncyCastle.Android.csproj +++ b/crypto/BouncyCastle.Android.csproj @@ -890,11 +890,13 @@ <Compile Include="src\crypto\modes\CbcBlockCipher.cs" /> <Compile Include="src\crypto\modes\CcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\CfbBlockCipher.cs" /> + <Compile Include="src\crypto\modes\ChaCha20Poly1305.cs" /> <Compile Include="src\crypto\modes\CtsBlockCipher.cs" /> <Compile Include="src\crypto\modes\EAXBlockCipher.cs" /> <Compile Include="src\crypto\modes\GCMBlockCipher.cs" /> <Compile Include="src\crypto\modes\GOFBBlockCipher.cs" /> <Compile Include="src\crypto\modes\IAeadBlockCipher.cs" /> + <Compile Include="src\crypto\modes\IAeadCipher.cs" /> <Compile Include="src\crypto\modes\KCcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\KCtrBlockCipher.cs" /> <Compile Include="src\crypto\modes\OCBBlockCipher.cs" /> diff --git a/crypto/BouncyCastle.csproj b/crypto/BouncyCastle.csproj index ab8559472..990d69553 100644 --- a/crypto/BouncyCastle.csproj +++ b/crypto/BouncyCastle.csproj @@ -884,11 +884,13 @@ <Compile Include="src\crypto\modes\CbcBlockCipher.cs" /> <Compile Include="src\crypto\modes\CcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\CfbBlockCipher.cs" /> + <Compile Include="src\crypto\modes\ChaCha20Poly1305.cs" /> <Compile Include="src\crypto\modes\CtsBlockCipher.cs" /> <Compile Include="src\crypto\modes\EAXBlockCipher.cs" /> <Compile Include="src\crypto\modes\GCMBlockCipher.cs" /> <Compile Include="src\crypto\modes\GOFBBlockCipher.cs" /> <Compile Include="src\crypto\modes\IAeadBlockCipher.cs" /> + <Compile Include="src\crypto\modes\IAeadCipher.cs" /> <Compile Include="src\crypto\modes\KCcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\KCtrBlockCipher.cs" /> <Compile Include="src\crypto\modes\OCBBlockCipher.cs" /> diff --git a/crypto/BouncyCastle.iOS.csproj b/crypto/BouncyCastle.iOS.csproj index 8bb7fe7b0..118bde49b 100644 --- a/crypto/BouncyCastle.iOS.csproj +++ b/crypto/BouncyCastle.iOS.csproj @@ -885,11 +885,13 @@ <Compile Include="src\crypto\modes\CbcBlockCipher.cs" /> <Compile Include="src\crypto\modes\CcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\CfbBlockCipher.cs" /> + <Compile Include="src\crypto\modes\ChaCha20Poly1305.cs" /> <Compile Include="src\crypto\modes\CtsBlockCipher.cs" /> <Compile Include="src\crypto\modes\EAXBlockCipher.cs" /> <Compile Include="src\crypto\modes\GCMBlockCipher.cs" /> <Compile Include="src\crypto\modes\GOFBBlockCipher.cs" /> <Compile Include="src\crypto\modes\IAeadBlockCipher.cs" /> + <Compile Include="src\crypto\modes\IAeadCipher.cs" /> <Compile Include="src\crypto\modes\KCcmBlockCipher.cs" /> <Compile Include="src\crypto\modes\KCtrBlockCipher.cs" /> <Compile Include="src\crypto\modes\OCBBlockCipher.cs" /> diff --git a/crypto/crypto.csproj b/crypto/crypto.csproj index 08e7b0ee1..de2dedf03 100644 --- a/crypto/crypto.csproj +++ b/crypto/crypto.csproj @@ -4304,6 +4304,11 @@ BuildAction = "Compile" /> <File + RelPath = "src\crypto\modes\ChaCha20Poly1305.cs" + SubType = "Code" + BuildAction = "Compile" + /> + <File RelPath = "src\crypto\modes\CTSBlockCipher.cs" SubType = "Code" BuildAction = "Compile" @@ -4329,6 +4334,11 @@ BuildAction = "Compile" /> <File + RelPath = "src\crypto\modes\IAeadCipher.cs" + SubType = "Code" + BuildAction = "Compile" + /> + <File RelPath = "src\crypto\modes\KCcmBlockCipher.cs" SubType = "Code" BuildAction = "Compile" @@ -12148,6 +12158,11 @@ BuildAction = "Compile" /> <File + RelPath = "test\src\crypto\test\ChaCha20Poly1305Test.cs" + SubType = "Code" + BuildAction = "Compile" + /> + <File RelPath = "test\src\crypto\test\ChaChaTest.cs" SubType = "Code" BuildAction = "Compile" diff --git a/crypto/src/crypto/modes/ChaCha20Poly1305.cs b/crypto/src/crypto/modes/ChaCha20Poly1305.cs new file mode 100644 index 000000000..9cc62174c --- /dev/null +++ b/crypto/src/crypto/modes/ChaCha20Poly1305.cs @@ -0,0 +1,551 @@ +using System; + +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; + +namespace Org.BouncyCastle.Crypto.Modes +{ + public class ChaCha20Poly1305 + : IAeadCipher + { + private enum State + { + Uninitialized = 0, + EncInit = 1, + EncAad = 2, + EncData = 3, + EncFinal = 4, + DecInit = 5, + DecAad = 6, + DecData = 7, + DecFinal = 8, + } + + private const int BufSize = 64; + private const int KeySize = 32; + private const int NonceSize = 12; + private const int MacSize = 16; + private static readonly byte[] Zeroes = new byte[MacSize - 1]; + + private const ulong AadLimit = ulong.MaxValue; + private const ulong DataLimit = ((1UL << 32) - 1) * 64; + + private readonly ChaCha7539Engine mChacha20; + private readonly IMac mPoly1305; + + private readonly byte[] mKey = new byte[KeySize]; + private readonly byte[] mNonce = new byte[NonceSize]; + private readonly byte[] mBuf = new byte[BufSize + MacSize]; + private readonly byte[] mMac = new byte[MacSize]; + + private byte[] mInitialAad; + + private ulong mAadCount; + private ulong mDataCount; + private State mState = State.Uninitialized; + private int mBufPos; + + public ChaCha20Poly1305() + : this(new Poly1305()) + { + } + + public ChaCha20Poly1305(IMac poly1305) + { + if (null == poly1305) + throw new ArgumentNullException("poly1305"); + if (MacSize != poly1305.GetMacSize()) + throw new ArgumentException("must be a 128-bit MAC", "poly1305"); + + this.mChacha20 = new ChaCha7539Engine(); + this.mPoly1305 = poly1305; + } + + public virtual string AlgorithmName + { + get { return "ChaCha20Poly1305"; } + } + + public virtual void Init(bool forEncryption, ICipherParameters parameters) + { + KeyParameter initKeyParam; + byte[] initNonce; + ICipherParameters chacha20Params; + + if (parameters is AeadParameters) + { + AeadParameters aeadParams = (AeadParameters)parameters; + + int macSizeBits = aeadParams.MacSize; + if ((MacSize * 8) != macSizeBits) + throw new ArgumentException("Invalid value for MAC size: " + macSizeBits); + + initKeyParam = aeadParams.Key; + initNonce = aeadParams.GetNonce(); + chacha20Params = new ParametersWithIV(initKeyParam, initNonce); + + this.mInitialAad = aeadParams.GetAssociatedText(); + } + else if (parameters is ParametersWithIV) + { + ParametersWithIV ivParams = (ParametersWithIV)parameters; + + initKeyParam = (KeyParameter)ivParams.Parameters; + initNonce = ivParams.GetIV(); + chacha20Params = ivParams; + + this.mInitialAad = null; + } + else + { + throw new ArgumentException("invalid parameters passed to ChaCha20Poly1305", "parameters"); + } + + // Validate key + if (null == initKeyParam) + { + if (State.Uninitialized == mState) + throw new ArgumentException("Key must be specified in initial init"); + } + else + { + if (KeySize != initKeyParam.GetKey().Length) + throw new ArgumentException("Key must be 256 bits"); + } + + // Validate nonce + if (null == initNonce || NonceSize != initNonce.Length) + throw new ArgumentException("Nonce must be 96 bits"); + + // Check for encryption with reused nonce + if (State.Uninitialized != mState && forEncryption && Arrays.AreEqual(mNonce, initNonce)) + { + if (null == initKeyParam || Arrays.AreEqual(mKey, initKeyParam.GetKey())) + throw new ArgumentException("cannot reuse nonce for ChaCha20Poly1305 encryption"); + } + + if (null != initKeyParam) + { + Array.Copy(initKeyParam.GetKey(), 0, mKey, 0, KeySize); + } + + Array.Copy(initNonce, 0, mNonce, 0, NonceSize); + + mChacha20.Init(true, chacha20Params); + + this.mState = forEncryption ? State.EncInit : State.DecInit; + + Reset(true, false); + } + + public virtual int GetOutputSize(int len) + { + int total = System.Math.Max(0, len) + mBufPos; + + switch (mState) + { + case State.DecInit: + case State.DecAad: + case State.DecData: + return System.Math.Max(0, total - MacSize); + case State.EncInit: + case State.EncAad: + case State.EncData: + return total + MacSize; + default: + throw new InvalidOperationException(); + } + } + + public virtual int GetUpdateOutputSize(int len) + { + int total = System.Math.Max(0, len) + mBufPos; + + switch (mState) + { + case State.DecInit: + case State.DecAad: + case State.DecData: + total = System.Math.Max(0, total - MacSize); + break; + case State.EncInit: + case State.EncAad: + case State.EncData: + break; + default: + throw new InvalidOperationException(); + } + + return total - (total % BufSize); + } + + public virtual void ProcessAadByte(byte input) + { + CheckAad(); + + this.mAadCount = IncrementCount(mAadCount, 1, AadLimit); + mPoly1305.Update(input); + } + + public virtual void ProcessAadBytes(byte[] inBytes, int inOff, int len) + { + if (null == inBytes) + throw new ArgumentNullException("inBytes"); + if (inOff < 0) + throw new ArgumentException("cannot be negative", "inOff"); + if (len < 0) + throw new ArgumentException("cannot be negative", "len"); + Check.DataLength(inBytes, inOff, len, "input buffer too short"); + + CheckAad(); + + if (len > 0) + { + this.mAadCount = IncrementCount(mAadCount, (uint)len, AadLimit); + mPoly1305.BlockUpdate(inBytes, inOff, len); + } + } + + public virtual int ProcessByte(byte input, byte[] outBytes, int outOff) + { + CheckData(); + + switch (mState) + { + case State.DecData: + { + mBuf[mBufPos] = input; + if (++mBufPos == mBuf.Length) + { + mPoly1305.BlockUpdate(mBuf, 0, BufSize); + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + Array.Copy(mBuf, BufSize, mBuf, 0, MacSize); + this.mBufPos = MacSize; + return BufSize; + } + + return 0; + } + case State.EncData: + { + mBuf[mBufPos] = input; + if (++mBufPos == BufSize) + { + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, BufSize); + this.mBufPos = 0; + return BufSize; + } + + return 0; + } + default: + throw new InvalidOperationException(); + } + } + + public virtual int ProcessBytes(byte[] inBytes, int inOff, int len, byte[] outBytes, int outOff) + { + if (null == inBytes) + throw new ArgumentNullException("inBytes"); + if (null == outBytes) + throw new ArgumentNullException("outBytes"); + if (inOff < 0) + throw new ArgumentException("cannot be negative", "inOff"); + if (len < 0) + throw new ArgumentException("cannot be negative", "len"); + Check.DataLength(inBytes, inOff, len, "input buffer too short"); + if (outOff < 0) + throw new ArgumentException("cannot be negative", "outOff"); + + CheckData(); + + int resultLen = 0; + + switch (mState) + { + case State.DecData: + { + for (int i = 0; i < len; ++i) + { + mBuf[mBufPos] = inBytes[inOff + i]; + if (++mBufPos == mBuf.Length) + { + mPoly1305.BlockUpdate(mBuf, 0, BufSize); + ProcessData(mBuf, 0, BufSize, outBytes, outOff + resultLen); + Array.Copy(mBuf, BufSize, mBuf, 0, MacSize); + this.mBufPos = MacSize; + resultLen += BufSize; + } + } + break; + } + case State.EncData: + { + if (mBufPos != 0) + { + while (len > 0) + { + --len; + mBuf[mBufPos] = inBytes[inOff++]; + if (++mBufPos == BufSize) + { + ProcessData(mBuf, 0, BufSize, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, BufSize); + this.mBufPos = 0; + resultLen = BufSize; + break; + } + } + } + + while (len >= BufSize) + { + ProcessData(inBytes, inOff, BufSize, outBytes, outOff + resultLen); + mPoly1305.BlockUpdate(outBytes, outOff + resultLen, BufSize); + inOff += BufSize; + len -= BufSize; + resultLen += BufSize; + } + + if (len > 0) + { + Array.Copy(inBytes, inOff, mBuf, 0, len); + this.mBufPos = len; + } + break; + } + default: + throw new InvalidOperationException(); + } + + return resultLen; + } + + public virtual int DoFinal(byte[] outBytes, int outOff) + { + if (null == outBytes) + throw new ArgumentNullException("outBytes"); + if (outOff < 0) + throw new ArgumentException("cannot be negative", "outOff"); + + CheckData(); + + Array.Clear(mMac, 0, MacSize); + + int resultLen = 0; + + switch (mState) + { + case State.DecData: + { + if (mBufPos < MacSize) + throw new InvalidCipherTextException("data too short"); + + resultLen = mBufPos - MacSize; + + Check.OutputLength(outBytes, outOff, resultLen, "output buffer too short"); + + if (resultLen > 0) + { + mPoly1305.BlockUpdate(mBuf, 0, resultLen); + ProcessData(mBuf, 0, resultLen, outBytes, outOff); + } + + FinishData(State.DecFinal); + + if (!Arrays.ConstantTimeAreEqual(MacSize, mMac, 0, mBuf, resultLen)) + { + throw new InvalidCipherTextException("mac check in ChaCha20Poly1305 failed"); + } + + break; + } + case State.EncData: + { + resultLen = mBufPos + MacSize; + + Check.OutputLength(outBytes, outOff, resultLen, "output buffer too short"); + + if (mBufPos > 0) + { + ProcessData(mBuf, 0, mBufPos, outBytes, outOff); + mPoly1305.BlockUpdate(outBytes, outOff, mBufPos); + } + + FinishData(State.EncFinal); + + Array.Copy(mMac, 0, outBytes, outOff + mBufPos, MacSize); + break; + } + default: + throw new InvalidOperationException(); + } + + Reset(false, true); + + return resultLen; + } + + public virtual byte[] GetMac() + { + return Arrays.Clone(mMac); + } + + public virtual void Reset() + { + Reset(true, true); + } + + private void CheckAad() + { + switch (mState) + { + case State.DecInit: + this.mState = State.DecAad; + break; + case State.EncInit: + this.mState = State.EncAad; + break; + case State.DecAad: + case State.EncAad: + break; + case State.EncFinal: + throw new InvalidOperationException("ChaCha20Poly1305 cannot be reused for encryption"); + default: + throw new InvalidOperationException(); + } + } + + private void CheckData() + { + switch (mState) + { + case State.DecInit: + case State.DecAad: + FinishAad(State.DecData); + break; + case State.EncInit: + case State.EncAad: + FinishAad(State.EncData); + break; + case State.DecData: + case State.EncData: + break; + case State.EncFinal: + throw new InvalidOperationException("ChaCha20Poly1305 cannot be reused for encryption"); + default: + throw new InvalidOperationException(); + } + } + + private void FinishAad(State nextState) + { + PadMac(mAadCount); + + this.mState = nextState; + } + + private void FinishData(State nextState) + { + PadMac(mDataCount); + + byte[] lengths = new byte[16]; + Pack.UInt64_To_LE(mAadCount, lengths, 0); + Pack.UInt64_To_LE(mDataCount, lengths, 8); + mPoly1305.BlockUpdate(lengths, 0, 16); + + mPoly1305.DoFinal(mMac, 0); + + this.mState = nextState; + } + + private ulong IncrementCount(ulong count, uint increment, ulong limit) + { + if (count > (limit - increment)) + throw new InvalidOperationException ("Limit exceeded"); + + return count + increment; + } + + private void InitMac() + { + byte[] firstBlock = new byte[64]; + try + { + mChacha20.ProcessBytes(firstBlock, 0, 64, firstBlock, 0); + mPoly1305.Init(new KeyParameter(firstBlock, 0, 32)); + } + finally + { + Array.Clear(firstBlock, 0, 64); + } + } + + private void PadMac(ulong count) + { + int partial = (int)count % MacSize; + if (0 != partial) + { + mPoly1305.BlockUpdate(Zeroes, 0, MacSize - partial); + } + } + + private void ProcessData(byte[] inBytes, int inOff, int inLen, byte[] outBytes, int outOff) + { + Check.OutputLength(outBytes, outOff, inLen, "output buffer too short"); + + mChacha20.ProcessBytes(inBytes, inOff, inLen, outBytes, outOff); + + this.mDataCount = IncrementCount(mDataCount, (uint)inLen, DataLimit); + } + + private void Reset(bool clearMac, bool resetCipher) + { + Array.Clear(mBuf, 0, mBuf.Length); + + if (clearMac) + { + Array.Clear(mMac, 0, mMac.Length); + } + + this.mAadCount = 0UL; + this.mDataCount = 0UL; + this.mBufPos = 0; + + switch (mState) + { + case State.DecInit: + case State.EncInit: + break; + case State.DecAad: + case State.DecData: + case State.DecFinal: + this.mState = State.DecInit; + break; + case State.EncAad: + case State.EncData: + case State.EncFinal: + this.mState = State.EncFinal; + return; + default: + throw new InvalidOperationException(); + } + + if (resetCipher) + { + mChacha20.Reset(); + } + + InitMac(); + + if (null != mInitialAad) + { + ProcessAadBytes(mInitialAad, 0, mInitialAad.Length); + } + } + } +} diff --git a/crypto/src/crypto/modes/IAeadBlockCipher.cs b/crypto/src/crypto/modes/IAeadBlockCipher.cs index 52c4ff428..ebe5ef234 100644 --- a/crypto/src/crypto/modes/IAeadBlockCipher.cs +++ b/crypto/src/crypto/modes/IAeadBlockCipher.cs @@ -1,105 +1,15 @@ -using Org.BouncyCastle.Crypto.Parameters; +using System; namespace Org.BouncyCastle.Crypto.Modes { - /// <summary> - /// A block cipher mode that includes authenticated encryption with a streaming mode - /// and optional associated data.</summary> - /// <see cref="AeadParameters"/> + /// <summary>An IAeadCipher based on an IBlockCipher.</summary> public interface IAeadBlockCipher + : IAeadCipher { - /// <summary>The name of the algorithm this cipher implements.</summary> - string AlgorithmName { get; } + /// <returns>The block size for this cipher, in bytes.</returns> + int GetBlockSize(); - /// <summary>The block cipher underlying this algorithm.</summary> + /// <summary>The block cipher underlying this algorithm.</summary> IBlockCipher GetUnderlyingCipher(); - - /// <summary>Initialise the cipher.</summary> - /// <remarks>Parameter can either be an AeadParameters or a ParametersWithIV object.</remarks> - /// <param name="forEncryption">Initialise for encryption if true, for decryption if false.</param> - /// <param name="parameters">The key or other data required by the cipher.</param> - void Init(bool forEncryption, ICipherParameters parameters); - - /// <returns>The block size for this cipher, in bytes.</returns> - int GetBlockSize(); - - /// <summary>Add a single byte to the associated data check.</summary> - /// <remarks>If the implementation supports it, this will be an online operation and will not retain the associated data.</remarks> - /// <param name="input">The byte to be processed.</param> - void ProcessAadByte(byte input); - - /// <summary>Add a sequence of bytes to the associated data check.</summary> - /// <remarks>If the implementation supports it, this will be an online operation and will not retain the associated data.</remarks> - /// <param name="inBytes">The input byte array.</param> - /// <param name="inOff">The offset into the input array where the data to be processed starts.</param> - /// <param name="len">The number of bytes to be processed.</param> - void ProcessAadBytes(byte[] inBytes, int inOff, int len); - - /** - * Encrypt/decrypt a single byte. - * - * @param input the byte to be processed. - * @param outBytes the output buffer the processed byte goes into. - * @param outOff the offset into the output byte array the processed data starts at. - * @return the number of bytes written to out. - * @exception DataLengthException if the output buffer is too small. - */ - int ProcessByte(byte input, byte[] outBytes, int outOff); - - /** - * Process a block of bytes from in putting the result into out. - * - * @param inBytes the input byte array. - * @param inOff the offset into the in array where the data to be processed starts. - * @param len the number of bytes to be processed. - * @param outBytes the output buffer the processed bytes go into. - * @param outOff the offset into the output byte array the processed data starts at. - * @return the number of bytes written to out. - * @exception DataLengthException if the output buffer is too small. - */ - int ProcessBytes(byte[] inBytes, int inOff, int len, byte[] outBytes, int outOff); - - /** - * Finish the operation either appending or verifying the MAC at the end of the data. - * - * @param outBytes space for any resulting output data. - * @param outOff offset into out to start copying the data at. - * @return number of bytes written into out. - * @throws InvalidOperationException if the cipher is in an inappropriate state. - * @throws InvalidCipherTextException if the MAC fails to match. - */ - int DoFinal(byte[] outBytes, int outOff); - - /** - * Return the value of the MAC associated with the last stream processed. - * - * @return MAC for plaintext data. - */ - byte[] GetMac(); - - /** - * Return the size of the output buffer required for a ProcessBytes - * an input of len bytes. - * - * @param len the length of the input. - * @return the space required to accommodate a call to ProcessBytes - * with len bytes of input. - */ - int GetUpdateOutputSize(int len); - - /** - * Return the size of the output buffer required for a ProcessBytes plus a - * DoFinal with an input of len bytes. - * - * @param len the length of the input. - * @return the space required to accommodate a call to ProcessBytes and DoFinal - * with len bytes of input. - */ - int GetOutputSize(int len); - - /// <summary> - /// Reset the cipher to the same state as it was after the last init (if there was one). - /// </summary> - void Reset(); } } diff --git a/crypto/src/crypto/modes/IAeadCipher.cs b/crypto/src/crypto/modes/IAeadCipher.cs new file mode 100644 index 000000000..437693cb6 --- /dev/null +++ b/crypto/src/crypto/modes/IAeadCipher.cs @@ -0,0 +1,111 @@ +using System; + +using Org.BouncyCastle.Crypto.Parameters; + +namespace Org.BouncyCastle.Crypto.Modes +{ + /// <summary> + /// A cipher mode that includes authenticated encryption with a streaming mode and optional + /// associated data. + /// </summary> + /// <remarks> + /// Implementations of this interface may operate in a packet mode (where all input data is + /// buffered and processed during the call to DoFinal, or in a streaming mode (where output + /// data is incrementally produced with each call to ProcessByte or ProcessBytes. This is + /// important to consider during decryption: in a streaming mode, unauthenticated plaintext + /// data may be output prior to the call to DoFinal that results in an authentication failure. + /// The higher level protocol utilising this cipher must ensure the plaintext data is handled + /// appropriately until the end of data is reached and the entire ciphertext is authenticated. + /// </remarks> + /// <see cref="AeadParameters"/> + public interface IAeadCipher + { + /// <summary>The name of the algorithm this cipher implements.</summary> + string AlgorithmName { get; } + + /// <summary>Initialise the cipher.</summary> + /// <remarks>Parameter can either be an AeadParameters or a ParametersWithIV object.</remarks> + /// <param name="forEncryption">Initialise for encryption if true, for decryption if false.</param> + /// <param name="parameters">The key or other data required by the cipher.</param> + void Init(bool forEncryption, ICipherParameters parameters); + + /// <summary>Add a single byte to the associated data check.</summary> + /// <remarks>If the implementation supports it, this will be an online operation and will not retain the associated data.</remarks> + /// <param name="input">The byte to be processed.</param> + void ProcessAadByte(byte input); + + /// <summary>Add a sequence of bytes to the associated data check.</summary> + /// <remarks>If the implementation supports it, this will be an online operation and will not retain the associated data.</remarks> + /// <param name="inBytes">The input byte array.</param> + /// <param name="inOff">The offset into the input array where the data to be processed starts.</param> + /// <param name="len">The number of bytes to be processed.</param> + void ProcessAadBytes(byte[] inBytes, int inOff, int len); + + /** + * Encrypt/decrypt a single byte. + * + * @param input the byte to be processed. + * @param outBytes the output buffer the processed byte goes into. + * @param outOff the offset into the output byte array the processed data starts at. + * @return the number of bytes written to out. + * @exception DataLengthException if the output buffer is too small. + */ + int ProcessByte(byte input, byte[] outBytes, int outOff); + + /** + * Process a block of bytes from in putting the result into out. + * + * @param inBytes the input byte array. + * @param inOff the offset into the in array where the data to be processed starts. + * @param len the number of bytes to be processed. + * @param outBytes the output buffer the processed bytes go into. + * @param outOff the offset into the output byte array the processed data starts at. + * @return the number of bytes written to out. + * @exception DataLengthException if the output buffer is too small. + */ + int ProcessBytes(byte[] inBytes, int inOff, int len, byte[] outBytes, int outOff); + + /** + * Finish the operation either appending or verifying the MAC at the end of the data. + * + * @param outBytes space for any resulting output data. + * @param outOff offset into out to start copying the data at. + * @return number of bytes written into out. + * @throws InvalidOperationException if the cipher is in an inappropriate state. + * @throws InvalidCipherTextException if the MAC fails to match. + */ + int DoFinal(byte[] outBytes, int outOff); + + /** + * Return the value of the MAC associated with the last stream processed. + * + * @return MAC for plaintext data. + */ + byte[] GetMac(); + + /** + * Return the size of the output buffer required for a ProcessBytes + * an input of len bytes. + * + * @param len the length of the input. + * @return the space required to accommodate a call to ProcessBytes + * with len bytes of input. + */ + int GetUpdateOutputSize(int len); + + /** + * Return the size of the output buffer required for a ProcessBytes plus a + * DoFinal with an input of len bytes. + * + * @param len the length of the input. + * @return the space required to accommodate a call to ProcessBytes and DoFinal + * with len bytes of input. + */ + int GetOutputSize(int len); + + /// <summary> + /// Reset the cipher to the same state as it was after the last init (if there was one). + /// </summary> + void Reset(); + } +} diff --git a/crypto/test/UnitTests.csproj b/crypto/test/UnitTests.csproj index 0ab02f02e..5b39de6ad 100644 --- a/crypto/test/UnitTests.csproj +++ b/crypto/test/UnitTests.csproj @@ -168,6 +168,7 @@ <Compile Include="src\crypto\test\BlowfishTest.cs" /> <Compile Include="src\crypto\test\CAST6Test.cs" /> <Compile Include="src\crypto\test\CCMTest.cs" /> + <Compile Include="src\crypto\test\ChaCha20Poly1305Test.cs" /> <Compile Include="src\crypto\test\ChaChaTest.cs" /> <Compile Include="src\crypto\test\CMacTest.cs" /> <Compile Include="src\crypto\test\CTSTest.cs" /> diff --git a/crypto/test/src/crypto/test/ChaCha20Poly1305Test.cs b/crypto/test/src/crypto/test/ChaCha20Poly1305Test.cs new file mode 100644 index 000000000..3f74669dc --- /dev/null +++ b/crypto/test/src/crypto/test/ChaCha20Poly1305Test.cs @@ -0,0 +1,443 @@ +using System; + +using NUnit.Framework; + +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Date; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; + +namespace Org.BouncyCastle.Crypto.Tests +{ + [TestFixture] + public class ChaCha20Poly1305Test + : SimpleTest + { + private static readonly string[][] TestVectors = new string[][] + { + new string[] + { + "Test Case 1", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "4c616469657320616e642047656e746c" + + "656d656e206f662074686520636c6173" + + "73206f66202739393a20496620492063" + + "6f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220" + + "746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069" + + "742e", + "50515253c0c1c2c3c4c5c6c7", + "070000004041424344454647", + "d31a8d34648e60db7b86afbc53ef7ec2" + + "a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b" + + "1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58" + + "fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b" + + "6116", + "1ae10b594f09e26a7e902ecbd0600691", + }, + }; + + public override string Name + { + get { return "ChaCha20Poly1305"; } + } + + public override void PerformTest() + { + for (int i = 0; i < TestVectors.Length; ++i) + { + RunTestCase(TestVectors[i]); + } + + OutputSizeTests(); + RandomTests(); + TestExceptions(); + } + + private void CheckTestCase( + ChaCha20Poly1305 encCipher, + ChaCha20Poly1305 decCipher, + string testName, + byte[] SA, + byte[] P, + byte[] C, + byte[] T) + { + 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 (enc.Length != len) + { + Fail("encryption reported incorrect length: " + testName); + } + + byte[] mac = encCipher.GetMac(); + + byte[] data = new byte[P.Length]; + Array.Copy(enc, 0, data, 0, data.Length); + byte[] tail = new byte[enc.Length - P.Length]; + Array.Copy(enc, P.Length, tail, 0, tail.Length); + + if (!AreEqual(C, data)) + { + Fail("incorrect encrypt in: " + testName); + } + + if (!AreEqual(T, mac)) + { + Fail("getMac() returned wrong mac in: " + testName); + } + + if (!AreEqual(T, tail)) + { + Fail("stream contained wrong mac in: " + testName); + } + + byte[] dec = new byte[decCipher.GetOutputSize(enc.Length)]; + if (SA != null) + { + decCipher.ProcessAadBytes(SA, 0, SA.Length); + } + len = decCipher.ProcessBytes(enc, 0, enc.Length, dec, 0); + len += decCipher.DoFinal(dec, len); + mac = decCipher.GetMac(); + + data = new byte[C.Length]; + Array.Copy(dec, 0, data, 0, data.Length); + + if (!AreEqual(P, data)) + { + Fail("incorrect decrypt in: " + testName); + } + } + + private ChaCha20Poly1305 InitCipher(bool forEncryption, AeadParameters parameters) + { + ChaCha20Poly1305 c = new ChaCha20Poly1305(); + c.Init(forEncryption, parameters); + return c; + } + + private static int NextInt(SecureRandom rand, int n) + { + if ((n & -n) == n) // i.e., n is a power of 2 + { + return (int)(((uint)n * (ulong)((uint)rand.NextInt() >> 1)) >> 31); + } + + int bits, value; + do + { + bits = (int)((uint)rand.NextInt() >> 1); + value = bits % n; + } + while (bits - value + (n - 1) < 0); + + return value; + } + + private void OutputSizeTests() + { + byte[] K = new byte[32]; + byte[] A = null; + byte[] N = new byte[12]; + + AeadParameters parameters = new AeadParameters(new KeyParameter(K), 16 * 8, N, A); + ChaCha20Poly1305 cipher = InitCipher(true, parameters); + + if (cipher.GetUpdateOutputSize(0) != 0) + { + Fail("incorrect getUpdateOutputSize for initial 0 bytes encryption"); + } + + if (cipher.GetOutputSize(0) != 16) + { + Fail("incorrect getOutputSize for initial 0 bytes encryption"); + } + + cipher.Init(false, parameters); + + if (cipher.GetUpdateOutputSize(0) != 0) + { + Fail("incorrect getUpdateOutputSize for initial 0 bytes decryption"); + } + + // NOTE: 0 bytes would be truncated data, but we want it to fail in the doFinal, not here + if (cipher.GetOutputSize(0) != 0) + { + Fail("fragile getOutputSize for initial 0 bytes decryption"); + } + + if (cipher.GetOutputSize(16) != 0) + { + Fail("incorrect getOutputSize for initial MAC-size bytes decryption"); + } + } + + private void RandomTests() + { + SecureRandom random = new SecureRandom(); + random.SetSeed(DateTimeUtilities.CurrentUnixMs()); + + for (int i = 0; i < 10; ++i) + { + RandomTest(random); + } + } + + private void RandomTest(SecureRandom random) + { + int kLength = 32; + byte[] K = new byte[kLength]; + random.NextBytes(K); + + int pLength = random.Next(65536); + byte[] P = new byte[pLength]; + random.NextBytes(P); + + int aLength = random.Next(256); + byte[] A = new byte[aLength]; + random.NextBytes(A); + + int saLength = random.Next(256); + byte[] SA = new byte[saLength]; + random.NextBytes(SA); + + int nonceLength = 12; + byte[] nonce = new byte[nonceLength]; + random.NextBytes(nonce); + + AeadParameters parameters = new AeadParameters(new KeyParameter(K), 16 * 8, nonce, A); + ChaCha20Poly1305 cipher = InitCipher(true, parameters); + byte[] C = new byte[cipher.GetOutputSize(P.Length)]; + int predicted = cipher.GetUpdateOutputSize(P.Length); + + int split = NextInt(random, SA.Length + 1); + cipher.ProcessAadBytes(SA, 0, split); + cipher.ProcessAadBytes(SA, split, SA.Length - split); + + int len = cipher.ProcessBytes(P, 0, P.Length, C, 0); + if (predicted != len) + { + Fail("encryption reported incorrect update length in randomised test"); + } + + len += cipher.DoFinal(C, len); + if (C.Length != len) + { + Fail("encryption reported incorrect length in randomised test"); + } + + byte[] encT = cipher.GetMac(); + byte[] tail = new byte[C.Length - P.Length]; + Array.Copy(C, P.Length, tail, 0, tail.Length); + + if (!AreEqual(encT, tail)) + { + Fail("stream contained wrong mac in randomised test"); + } + + cipher.Init(false, parameters); + byte[] decP = new byte[cipher.GetOutputSize(C.Length)]; + predicted = cipher.GetUpdateOutputSize(C.Length); + + split = NextInt(random, SA.Length + 1); + cipher.ProcessAadBytes(SA, 0, split); + cipher.ProcessAadBytes(SA, split, SA.Length - split); + + len = cipher.ProcessBytes(C, 0, C.Length, decP, 0); + if (predicted != len) + { + Fail("decryption reported incorrect update length in randomised test"); + } + + len += cipher.DoFinal(decP, len); + + if (!AreEqual(P, decP)) + { + Fail("incorrect decrypt in randomised test"); + } + + byte[] decT = cipher.GetMac(); + if (!AreEqual(encT, decT)) + { + Fail("decryption produced different mac from encryption"); + } + + // + // key reuse test + // + cipher.Init(false, AeadTestUtilities.ReuseKey(parameters)); + decP = new byte[cipher.GetOutputSize(C.Length)]; + + split = NextInt(random, SA.Length + 1); + cipher.ProcessAadBytes(SA, 0, split); + cipher.ProcessAadBytes(SA, split, SA.Length - split); + + len = cipher.ProcessBytes(C, 0, C.Length, decP, 0); + len += cipher.DoFinal(decP, len); + + if (!AreEqual(P, decP)) + { + Fail("incorrect decrypt in randomised test"); + } + + decT = cipher.GetMac(); + if (!AreEqual(encT, decT)) + { + Fail("decryption produced different mac from encryption"); + } + } + + private void RunTestCase(string[] testVector) + { + int pos = 0; + string testName = testVector[pos++]; + byte[] K = Hex.DecodeStrict(testVector[pos++]); + byte[] P = Hex.DecodeStrict(testVector[pos++]); + byte[] A = Hex.DecodeStrict(testVector[pos++]); + byte[] N = Hex.DecodeStrict(testVector[pos++]); + byte[] C = Hex.DecodeStrict(testVector[pos++]); + byte[] T = Hex.DecodeStrict(testVector[pos++]); + + RunTestCase(testName, K, N, A, P, C, T); + } + + private void RunTestCase( + string testName, + byte[] K, + byte[] N, + byte[] A, + byte[] P, + byte[] C, + byte[] T) + { + byte[] fa = new byte[A.Length / 2]; + byte[] la = new byte[A.Length - (A.Length / 2)]; + Array.Copy(A, 0, fa, 0, fa.Length); + Array.Copy(A, fa.Length, la, 0, la.Length); + + RunTestCase(testName + " all initial associated data", K, N, A, null, P, C, T); + RunTestCase(testName + " all subsequent associated data", K, N, null, A, P, C, T); + RunTestCase(testName + " split associated data", K, N, fa, la, P, C, T); + } + + private void RunTestCase( + string testName, + byte[] K, + byte[] N, + byte[] A, + byte[] SA, + byte[] P, + byte[] C, + byte[] T) + { + AeadParameters parameters = new AeadParameters(new KeyParameter(K), T.Length * 8, N, A); + ChaCha20Poly1305 encCipher = InitCipher(true, parameters); + ChaCha20Poly1305 decCipher = InitCipher(false, parameters); + CheckTestCase(encCipher, decCipher, testName, SA, P, C, T); + encCipher = InitCipher(true, parameters); + CheckTestCase(encCipher, decCipher, testName + " (reused)", SA, P, C, T); + + // Key reuse + AeadParameters keyReuseParams = AeadTestUtilities.ReuseKey(parameters); + + try + { + encCipher.Init(true, keyReuseParams); + Fail("no exception"); + } + catch (ArgumentException e) + { + IsTrue("wrong message", "cannot reuse nonce for ChaCha20Poly1305 encryption".Equals(e.Message)); + } + } + + private void TestExceptions() + { + ChaCha20Poly1305 c = new ChaCha20Poly1305(); + + try + { + c = new ChaCha20Poly1305(new SipHash()); + + Fail("incorrect mac size not picked up"); + } + catch (ArgumentException e) + { + // expected + } + + try + { + c.Init(false, new KeyParameter(new byte[32])); + + Fail("illegal argument not picked up"); + } + catch (ArgumentException e) + { + // expected + } + + AeadTestUtilities.TestTampering(this, c, new AeadParameters(new KeyParameter(new byte[32]), 128, new byte[12])); + + byte[] P = Strings.ToByteArray("Hello world!"); + byte[] buf = new byte[100]; + + c = new ChaCha20Poly1305(); + AeadParameters aeadParameters = new AeadParameters(new KeyParameter(new byte[32]), 128, new byte[12]); + c.Init(true, aeadParameters); + + c.ProcessBytes(P, 0, P.Length, buf, 0); + + c.DoFinal(buf, 0); + + try + { + c.DoFinal(buf, 0); + Fail("no exception on reuse"); + } + catch (InvalidOperationException e) + { + IsTrue("wrong message", e.Message.Equals("ChaCha20Poly1305 cannot be reused for encryption")); + } + + try + { + c.Init(true, aeadParameters); + Fail("no exception on reuse"); + } + catch (ArgumentException e) + { + IsTrue("wrong message", e.Message.Equals("cannot reuse nonce for ChaCha20Poly1305 encryption")); + } + } + + public static void Main(string[] args) + { + RunTest(new ChaCha20Poly1305Test()); + } + + [Test] + public void TestFunction() + { + string resultText = Perform().ToString(); + + Assert.AreEqual(Name + ": Okay", resultText); + } + } +} diff --git a/crypto/test/src/crypto/test/RegressionTest.cs b/crypto/test/src/crypto/test/RegressionTest.cs index 2bc55e8a2..aa9cd0483 100644 --- a/crypto/test/src/crypto/test/RegressionTest.cs +++ b/crypto/test/src/crypto/test/RegressionTest.cs @@ -105,6 +105,7 @@ namespace Org.BouncyCastle.Crypto.Tests new Salsa20Test(), new XSalsa20Test(), new ChaChaTest(), + new ChaCha20Poly1305Test(), new CMacTest(), new EaxTest(), new GcmTest(), |