#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR) #pragma warning disable using System; using System.IO; using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Engines; using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Modes.Gcm; using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Parameters; using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Utilities; using BestHTTP.SecureProtocol.Org.BouncyCastle.Utilities; using BestHTTP.SecureProtocol.Org.BouncyCastle.Utilities.IO; namespace BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Modes { /** * GCM-SIV Mode. *

It should be noted that the specified limit of 236 bytes is not supported. This is because all bytes are * cached in a ByteArrayOutputStream object (which has a limit of a little less than 231 bytes), * and are output on the DoFinal() call (which can only process a maximum of 231 bytes).

*

The practical limit of 231 - 24 bytes is policed, and attempts to breach the limit will be rejected

*

In order to properly support the higher limit, an extended form of ByteArrayOutputStream would be needed * which would use multiple arrays to store the data. In addition, a new doOutput method would be required (similar * to that in XOF digests), which would allow the data to be output over multiple calls. Alternatively an extended * form of ByteArrayInputStream could be used to deliver the data.

*/ public class GcmSivBlockCipher : IAeadBlockCipher { /// The buffer length. private static readonly int BUFLEN = 16; /// The halfBuffer length. private static readonly int HALFBUFLEN = BUFLEN >> 1; /// The nonce length. private static readonly int NONCELEN = 12; /** * The maximum data length (AEAD/PlainText). Due to implementation constraints this is restricted to the maximum * array length (https://programming.guide/java/array-maximum-length.html) minus the BUFLEN to allow for the MAC */ private static readonly int MAX_DATALEN = Int32.MaxValue - 8 - BUFLEN; /** * The top bit mask. */ private static readonly byte MASK = (byte)0x80; /** * The addition constant. */ private static readonly byte ADD = (byte)0xE1; /** * The initialisation flag. */ private static readonly int INIT = 1; /** * The aeadComplete flag. */ private static readonly int AEAD_COMPLETE = 2; /** * The cipher. */ private readonly IBlockCipher theCipher; /** * The multiplier. */ private readonly IGcmMultiplier theMultiplier; /** * The gHash buffer. */ internal readonly byte[] theGHash = new byte[BUFLEN]; /** * The reverse buffer. */ internal readonly byte[] theReverse = new byte[BUFLEN]; /** * The aeadHasher. */ private readonly GcmSivHasher theAEADHasher; /** * The dataHasher. */ private readonly GcmSivHasher theDataHasher; /** * The plainDataStream. */ private GcmSivCache thePlain; /** * The encryptedDataStream (decryption only). */ private GcmSivCache theEncData; /** * Are we encrypting? */ private bool forEncryption; /** * The initialAEAD. */ private byte[] theInitialAEAD; /** * The nonce. */ private byte[] theNonce; /** * The flags. */ private int theFlags; /** * Constructor. */ public GcmSivBlockCipher() : this(new AesEngine()) { } /** * Constructor. * @param pCipher the underlying cipher */ public GcmSivBlockCipher(IBlockCipher pCipher) : this(pCipher, new Tables4kGcmMultiplier()) { } /** * Constructor. * @param pCipher the underlying cipher * @param pMultiplier the multiplier */ public GcmSivBlockCipher(IBlockCipher pCipher, IGcmMultiplier pMultiplier) { /* Ensure that the cipher is the correct size */ if (pCipher.GetBlockSize() != BUFLEN) throw new ArgumentException("Cipher required with a block size of " + BUFLEN + "."); /* Store parameters */ theCipher = pCipher; theMultiplier = pMultiplier; /* Create the hashers */ theAEADHasher = new GcmSivHasher(this); theDataHasher = new GcmSivHasher(this); } public virtual IBlockCipher GetUnderlyingCipher() { return theCipher; } public virtual int GetBlockSize() { return theCipher.GetBlockSize(); } public virtual void Init(bool pEncrypt, ICipherParameters cipherParameters) { /* Set defaults */ byte[] myInitialAEAD = null; byte[] myNonce = null; KeyParameter myKey = null; /* Access parameters */ if (cipherParameters is AeadParameters) { AeadParameters myAEAD = (AeadParameters)cipherParameters; myInitialAEAD = myAEAD.GetAssociatedText(); myNonce = myAEAD.GetNonce(); myKey = myAEAD.Key; } else if (cipherParameters is ParametersWithIV) { ParametersWithIV myParms = (ParametersWithIV)cipherParameters; myNonce = myParms.GetIV(); myKey = (KeyParameter)myParms.Parameters; } else { throw new ArgumentException("invalid parameters passed to GCM_SIV"); } /* Check nonceSize */ if (myNonce == null || myNonce.Length != NONCELEN) { throw new ArgumentException("Invalid nonce"); } /* Check keysize */ if (myKey == null) { throw new ArgumentException("Invalid key"); } byte[] k = myKey.GetKey(); if (k.Length != BUFLEN && k.Length != (BUFLEN << 1)) { throw new ArgumentException("Invalid key"); } /* Reset details */ forEncryption = pEncrypt; theInitialAEAD = myInitialAEAD; theNonce = myNonce; /* Initialise the keys */ deriveKeys(myKey); ResetStreams(); } public virtual string AlgorithmName { get { return theCipher.AlgorithmName + "-GCM-SIV"; } } /** * check AEAD status. * @param pLen the aeadLength */ private void CheckAeadStatus(int pLen) { /* Check we are initialised */ if ((theFlags & INIT) == 0) { throw new InvalidOperationException("Cipher is not initialised"); } /* Check AAD is allowed */ if ((theFlags & AEAD_COMPLETE) != 0) { throw new InvalidOperationException("AEAD data cannot be processed after ordinary data"); } /* Make sure that we haven't breached AEAD data limit */ if ((long)theAEADHasher.getBytesProcessed() + Int64.MinValue > (MAX_DATALEN - pLen) + Int64.MinValue) { throw new InvalidOperationException("AEAD byte count exceeded"); } } /** * check status. * @param pLen the dataLength */ private void CheckStatus(int pLen) { /* Check we are initialised */ if ((theFlags & INIT) == 0) { throw new InvalidOperationException("Cipher is not initialised"); } /* Complete the AEAD section if this is the first data */ if ((theFlags & AEAD_COMPLETE) == 0) { theAEADHasher.completeHash(); theFlags |= AEAD_COMPLETE; } /* Make sure that we haven't breached data limit */ long dataLimit = MAX_DATALEN; long currBytes = thePlain.Length; if (!forEncryption) { dataLimit += BUFLEN; currBytes = theEncData.Length; } if (currBytes + System.Int64.MinValue > (dataLimit - pLen) + System.Int64.MinValue) { throw new InvalidOperationException("byte count exceeded"); } } public virtual void ProcessAadByte(byte pByte) { /* Check that we can supply AEAD */ CheckAeadStatus(1); /* Process the aead */ theAEADHasher.updateHash(pByte); } public virtual void ProcessAadBytes(byte[] pData, int pOffset, int pLen) { /* Check that we can supply AEAD */ CheckAeadStatus(pLen); /* Check input buffer */ CheckBuffer(pData, pOffset, pLen, false); /* Process the aead */ theAEADHasher.updateHash(pData, pOffset, pLen); } public virtual int ProcessByte(byte pByte, byte[] pOutput, int pOutOffset) { /* Check that we have initialised */ CheckStatus(1); /* Store the data */ if (forEncryption) { thePlain.WriteByte(pByte); theDataHasher.updateHash(pByte); } else { theEncData.WriteByte(pByte); } /* No data returned */ return 0; } public virtual int ProcessBytes(byte[] pData, int pOffset, int pLen, byte[] pOutput, int pOutOffset) { /* Check that we have initialised */ CheckStatus(pLen); /* Check input buffer */ CheckBuffer(pData, pOffset, pLen, false); /* Store the data */ if (forEncryption) { thePlain.Write(pData, pOffset, pLen); theDataHasher.updateHash(pData, pOffset, pLen); } else { theEncData.Write(pData, pOffset, pLen); } /* No data returned */ return 0; } public virtual int DoFinal(byte[] pOutput, int pOffset) { /* Check that we have initialised */ CheckStatus(0); /* Check output buffer */ CheckBuffer(pOutput, pOffset, GetOutputSize(0), true); /* If we are encrypting */ if (forEncryption) { /* Derive the tag */ byte[] myTag = calculateTag(); /* encrypt the plain text */ int myDataLen = BUFLEN + encryptPlain(myTag, pOutput, pOffset); /* Add the tag to the output */ Array.Copy(myTag, 0, pOutput, pOffset + (int)thePlain.Length, BUFLEN); /* Reset the streams */ ResetStreams(); return myDataLen; /* else we are decrypting */ } else { /* decrypt to plain text */ decryptPlain(); /* Release plain text */ int myDataLen = Streams.WriteBufTo(thePlain, pOutput, pOffset); /* Reset the streams */ ResetStreams(); return myDataLen; } } public virtual byte[] GetMac() { throw new InvalidOperationException(); } public virtual int GetUpdateOutputSize(int pLen) { return 0; } public virtual int GetOutputSize(int pLen) { if (forEncryption) { return (int)(pLen + thePlain.Length + BUFLEN); } int myCurr = (int)(pLen + theEncData.Length); return myCurr > BUFLEN ? myCurr - BUFLEN : 0; } public virtual void Reset() { ResetStreams(); } /** * Reset Streams. */ private void ResetStreams() { /* Clear the plainText buffer */ if (thePlain != null) { thePlain.Position = 0L; Streams.WriteZeroes(thePlain, thePlain.Capacity); } /* Reset hashers */ theAEADHasher.Reset(); theDataHasher.Reset(); /* Recreate streams (to release memory) */ thePlain = new GcmSivCache(); theEncData = forEncryption ? null : new GcmSivCache(); /* Initialise AEAD if required */ theFlags &= ~AEAD_COMPLETE; Arrays.Fill(theGHash, (byte)0); if (theInitialAEAD != null) { theAEADHasher.updateHash(theInitialAEAD, 0, theInitialAEAD.Length); } } /** * Obtain buffer length (allowing for null). * @param pBuffer the buffere * @return the length */ private static int bufLength(byte[] pBuffer) { return pBuffer == null ? 0 : pBuffer.Length; } /** * Check buffer. * @param pBuffer the buffer * @param pOffset the offset * @param pLen the length * @param pOutput is this an output buffer? */ private static void CheckBuffer(byte[] pBuffer, int pOffset, int pLen, bool pOutput) { /* Access lengths */ int myBufLen = bufLength(pBuffer); int myLast = pOffset + pLen; /* Check for negative values and buffer overflow */ bool badLen = pLen < 0 || pOffset < 0 || myLast < 0; if (badLen || myLast > myBufLen) { throw pOutput ? new OutputLengthException("Output buffer too short.") : new DataLengthException("Input buffer too short."); } } /** * encrypt data stream. * @param pCounter the counter * @param pTarget the target buffer * @param pOffset the target offset * @return the length of data encrypted */ private int encryptPlain(byte[] pCounter, byte[] pTarget, int pOffset) { /* Access buffer and length */ #if PORTABLE || NETFX_CORE byte[] thePlainBuf = thePlain.ToArray(); int thePlainLen = thePlainBuf.Length; #else byte[] thePlainBuf = thePlain.GetBuffer(); int thePlainLen = (int)thePlain.Length; #endif byte[] mySrc = thePlainBuf; byte[] myCounter = Arrays.Clone(pCounter); myCounter[BUFLEN - 1] |= MASK; byte[] myMask = new byte[BUFLEN]; long myRemaining = thePlainLen; int myOff = 0; /* While we have data to process */ while (myRemaining > 0) { /* Generate the next mask */ theCipher.ProcessBlock(myCounter, 0, myMask, 0); /* Xor data into mask */ int myLen = (int)System.Math.Min(BUFLEN, myRemaining); xorBlock(myMask, mySrc, myOff, myLen); /* Copy encrypted data to output */ Array.Copy(myMask, 0, pTarget, pOffset + myOff, myLen); /* Adjust counters */ myRemaining -= myLen; myOff += myLen; incrementCounter(myCounter); } /* Return the amount of data processed */ return thePlainLen; } /** * decrypt data stream. * @throws InvalidCipherTextException on data too short or mac check failed */ private void decryptPlain() { /* Access buffer and length */ #if PORTABLE || NETFX_CORE byte[] theEncDataBuf = theEncData.ToArray(); int theEncDataLen = theEncDataBuf.Length; #else byte[] theEncDataBuf = theEncData.GetBuffer(); int theEncDataLen = (int)theEncData.Length; #endif byte[] mySrc = theEncDataBuf; int myRemaining = theEncDataLen - BUFLEN; /* Check for insufficient data */ if (myRemaining < 0) { throw new InvalidCipherTextException("Data too short"); } /* Access counter */ byte[] myExpected = Arrays.CopyOfRange(mySrc, myRemaining, myRemaining + BUFLEN); byte[] myCounter = Arrays.Clone(myExpected); myCounter[BUFLEN - 1] |= MASK; byte[] myMask = new byte[BUFLEN]; int myOff = 0; /* While we have data to process */ while (myRemaining > 0) { /* Generate the next mask */ theCipher.ProcessBlock(myCounter, 0, myMask, 0); /* Xor data into mask */ int myLen = System.Math.Min(BUFLEN, myRemaining); xorBlock(myMask, mySrc, myOff, myLen); /* Write data to plain dataStream */ thePlain.Write(myMask, 0, myLen); theDataHasher.updateHash(myMask, 0, myLen); /* Adjust counters */ myRemaining -= myLen; myOff += myLen; incrementCounter(myCounter); } /* Derive and check the tag */ byte[] myTag = calculateTag(); if (!Arrays.ConstantTimeAreEqual(myTag, myExpected)) { Reset(); throw new InvalidCipherTextException("mac check failed"); } } /** * calculate tag. * @return the calculated tag */ private byte[] calculateTag() { /* Complete the hash */ theDataHasher.completeHash(); byte[] myPolyVal = completePolyVal(); /* calculate polyVal */ byte[] myResult = new byte[BUFLEN]; /* Fold in the nonce */ for (int i = 0; i < NONCELEN; i++) { myPolyVal[i] ^= theNonce[i]; } /* Clear top bit */ myPolyVal[BUFLEN - 1] &= (byte)(MASK - 1); /* Calculate tag and return it */ theCipher.ProcessBlock(myPolyVal, 0, myResult, 0); return myResult; } /** * complete polyVAL. * @return the calculated value */ private byte[] completePolyVal() { /* Build the polyVal result */ byte[] myResult = new byte[BUFLEN]; gHashLengths(); fillReverse(theGHash, 0, BUFLEN, myResult); return myResult; } /** * process lengths. */ private void gHashLengths() { /* Create reversed bigEndian buffer to keep it simple */ byte[] myIn = new byte[BUFLEN]; Pack.UInt64_To_BE((ulong)Bytes.NumBits * theDataHasher.getBytesProcessed(), myIn, 0); Pack.UInt64_To_BE((ulong)Bytes.NumBits * theAEADHasher.getBytesProcessed(), myIn, Longs.NumBytes); /* hash value */ gHASH(myIn); } /** * perform the next GHASH step. * @param pNext the next value */ private void gHASH(byte[] pNext) { xorBlock(theGHash, pNext); theMultiplier.MultiplyH(theGHash); } /** * Byte reverse a buffer. * @param pInput the input buffer * @param pOffset the offset * @param pLength the length of data (<= BUFLEN) * @param pOutput the output buffer */ private static void fillReverse(byte[] pInput, int pOffset, int pLength, byte[] pOutput) { /* Loop through the buffer */ for (int i = 0, j = BUFLEN - 1; i < pLength; i++, j--) { /* Copy byte */ pOutput[j] = pInput[pOffset + i]; } } /** * xor a full block buffer. * @param pLeft the left operand and result * @param pRight the right operand */ private static void xorBlock(byte[] pLeft, byte[] pRight) { /* Loop through the bytes */ for (int i = 0; i < BUFLEN; i++) { pLeft[i] ^= pRight[i]; } } /** * xor a partial block buffer. * @param pLeft the left operand and result * @param pRight the right operand * @param pOffset the offset in the right operand * @param pLength the length of data in the right operand */ private static void xorBlock(byte[] pLeft, byte[] pRight, int pOffset, int pLength) { /* Loop through the bytes */ for (int i = 0; i < pLength; i++) { pLeft[i] ^= pRight[i + pOffset]; } } /** * increment the counter. * @param pCounter the counter to increment */ private static void incrementCounter(byte[] pCounter) { /* Loop through the bytes incrementing counter */ for (int i = 0; i < Integers.NumBytes; i++) { if (++pCounter[i] != 0) { break; } } } /** * multiply by X. * @param pValue the value to adjust */ private static void mulX(byte[] pValue) { /* Loop through the bytes */ byte myMask = (byte)0; for (int i = 0; i < BUFLEN; i++) { byte myValue = pValue[i]; pValue[i] = (byte)(((myValue >> 1) & ~MASK) | myMask); myMask = (byte)((myValue & 1) == 0 ? (byte)0 : MASK); } /* Xor in addition if last bit was set */ if (myMask != 0) { pValue[0] ^= ADD; } } /** * Derive Keys. * @param pKey the keyGeneration key */ private void deriveKeys(KeyParameter pKey) { /* Create the buffers */ byte[] myIn = new byte[BUFLEN]; byte[] myOut = new byte[BUFLEN]; byte[] myResult = new byte[BUFLEN]; byte[] myEncKey = new byte[pKey.GetKey().Length]; /* Prepare for encryption */ Array.Copy(theNonce, 0, myIn, BUFLEN - NONCELEN, NONCELEN); theCipher.Init(true, pKey); /* Derive authentication key */ int myOff = 0; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myResult, myOff, HALFBUFLEN); myIn[0]++; myOff += HALFBUFLEN; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myResult, myOff, HALFBUFLEN); /* Derive encryption key */ myIn[0]++; myOff = 0; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myEncKey, myOff, HALFBUFLEN); myIn[0]++; myOff += HALFBUFLEN; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myEncKey, myOff, HALFBUFLEN); /* If we have a 32byte key */ if (myEncKey.Length == BUFLEN << 1) { /* Derive remainder of encryption key */ myIn[0]++; myOff += HALFBUFLEN; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myEncKey, myOff, HALFBUFLEN); myIn[0]++; myOff += HALFBUFLEN; theCipher.ProcessBlock(myIn, 0, myOut, 0); Array.Copy(myOut, 0, myEncKey, myOff, HALFBUFLEN); } /* Initialise the Cipher */ theCipher.Init(true, new KeyParameter(myEncKey)); /* Initialise the multiplier */ fillReverse(myResult, 0, BUFLEN, myOut); mulX(myOut); theMultiplier.Init(myOut); theFlags |= INIT; } private class GcmSivCache : MemoryStream { internal GcmSivCache() { } } /** * Hash Control. */ private class GcmSivHasher { /** * Cache. */ private readonly byte[] theBuffer = new byte[BUFLEN]; /** * Single byte cache. */ private readonly byte[] theByte = new byte[1]; /** * Count of active bytes in cache. */ private int numActive; /** * Count of hashed bytes. */ private ulong numHashed; private readonly GcmSivBlockCipher parent; internal GcmSivHasher(GcmSivBlockCipher parent) { this.parent = parent; } /** * Obtain the count of bytes hashed. * @return the count */ internal ulong getBytesProcessed() { return numHashed; } /** * Reset the hasher. */ internal void Reset() { numActive = 0; numHashed = 0; } /** * update hash. * @param pByte the byte */ internal void updateHash(byte pByte) { theByte[0] = pByte; updateHash(theByte, 0, 1); } /** * update hash. * @param pBuffer the buffer * @param pOffset the offset within the buffer * @param pLen the length of data */ internal void updateHash(byte[] pBuffer, int pOffset, int pLen) { /* If we should process the cache */ int mySpace = BUFLEN - numActive; int numProcessed = 0; int myRemaining = pLen; if (numActive > 0 && pLen >= mySpace) { /* Copy data into the cache and hash it */ Array.Copy(pBuffer, pOffset, theBuffer, numActive, mySpace); fillReverse(theBuffer, 0, BUFLEN, parent.theReverse); parent.gHASH(parent.theReverse); /* Adjust counters */ numProcessed += mySpace; myRemaining -= mySpace; numActive = 0; } /* While we have full blocks */ while (myRemaining >= BUFLEN) { /* Access the next data */ fillReverse(pBuffer, pOffset + numProcessed, BUFLEN, parent.theReverse); parent.gHASH(parent.theReverse); /* Adjust counters */ numProcessed += mySpace; myRemaining -= mySpace; } /* If we have remaining data */ if (myRemaining > 0) { /* Copy data into the cache */ Array.Copy(pBuffer, pOffset + numProcessed, theBuffer, numActive, myRemaining); numActive += myRemaining; } /* Adjust the number of bytes processed */ numHashed += (ulong)pLen; } /** * complete hash. */ internal void completeHash() { /* If we have remaining data */ if (numActive > 0) { /* Access the next data */ Arrays.Fill(parent.theReverse, (byte)0); fillReverse(theBuffer, 0, numActive, parent.theReverse); /* hash value */ parent.gHASH(parent.theReverse); } } } } } #pragma warning restore #endif