290 lines
15 KiB
C#
290 lines
15 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using Microsoft.AspNet.Cryptography;
|
|
using Microsoft.AspNet.Cryptography.Cng;
|
|
using Microsoft.AspNet.Cryptography.SafeHandles;
|
|
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
|
|
using Microsoft.AspNet.DataProtection.Cng.Internal;
|
|
using Microsoft.AspNet.DataProtection.SP800_108;
|
|
|
|
namespace Microsoft.AspNet.DataProtection.Cng
|
|
{
|
|
// GCM is defined in NIST SP 800-38D (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf).
|
|
// Heed closely the uniqueness requirements called out in Sec. 8: the probability that the GCM encryption
|
|
// routine is ever invoked on two or more distinct sets of input data with the same (key, IV) shall not
|
|
// exceed 2^-32. If we fix the key and use a random 96-bit IV for each invocation, this means that after
|
|
// 2^32 encryption operations the odds of reusing any (key, IV) pair is 2^-32 (see Sec. 8.3). This won't
|
|
// work for our use since a high-traffic web server can go through 2^32 requests in mere days. Instead,
|
|
// we'll use 224 bits of entropy for each operation, with 128 bits going to the KDF and 96 bits
|
|
// going to the IV. This means that we'll only hit the 2^-32 probability limit after 2^96 encryption
|
|
// operations, which will realistically never happen. (At the absurd rate of one encryption operation
|
|
// per nanosecond, it would still take 180 times the age of the universe to hit 2^96 operations.)
|
|
internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase
|
|
{
|
|
// Having a key modifier ensures with overwhelming probability that no two encryption operations
|
|
// will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's
|
|
// ability to mount a key-dependent chosen ciphertext attack. See also the class-level comment
|
|
// for how this is used to overcome GCM's IV limitations.
|
|
private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
|
|
|
|
private const uint NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV
|
|
private const uint TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size
|
|
|
|
private readonly byte[] _contextHeader;
|
|
private readonly IBCryptGenRandom _genRandom;
|
|
private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider;
|
|
private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
|
|
private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;
|
|
|
|
public GcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom genRandom = null)
|
|
{
|
|
// Is the key size appropriate?
|
|
AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked(symmetricAlgorithmKeySizeInBytes * 8));
|
|
CryptoUtil.Assert(symmetricAlgorithmHandle.GetCipherBlockLength() == 128 / 8, "GCM requires a block cipher algorithm with a 128-bit block size.");
|
|
|
|
_genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
|
|
_sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
|
|
_symmetricAlgorithmHandle = symmetricAlgorithmHandle;
|
|
_symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
|
|
_contextHeader = CreateContextHeader();
|
|
}
|
|
|
|
private byte[] CreateContextHeader()
|
|
{
|
|
byte[] retVal = new byte[checked(
|
|
1 /* KDF alg */
|
|
+ 1 /* chaining mode */
|
|
+ sizeof(uint) /* sym alg key size */
|
|
+ sizeof(uint) /* GCM nonce size */
|
|
+ sizeof(uint) /* sym alg block size */
|
|
+ sizeof(uint) /* GCM tag size */
|
|
+ TAG_SIZE_IN_BYTES /* tag of GCM-encrypted empty string */)];
|
|
|
|
fixed (byte* pbRetVal = retVal)
|
|
{
|
|
byte* ptr = pbRetVal;
|
|
|
|
// First is the two-byte header
|
|
*(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
|
|
*(ptr++) = 1; // 0x01 = GCM encryption + authentication
|
|
|
|
// Next is information about the symmetric algorithm (key size, nonce size, block size, tag size)
|
|
BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes);
|
|
BitHelpers.WriteTo(ref ptr, NONCE_SIZE_IN_BYTES);
|
|
BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); // block size = tag size
|
|
BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES);
|
|
|
|
// See the design document for an explanation of the following code.
|
|
byte[] tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
|
|
fixed (byte* pbTempKeys = tempKeys)
|
|
{
|
|
byte dummy;
|
|
|
|
// Derive temporary key for encryption.
|
|
using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider())
|
|
{
|
|
provider.DeriveKey(
|
|
pbLabel: &dummy,
|
|
cbLabel: 0,
|
|
pbContext: &dummy,
|
|
cbContext: 0,
|
|
pbDerivedKey: pbTempKeys,
|
|
cbDerivedKey: (uint)tempKeys.Length);
|
|
}
|
|
|
|
// Encrypt a zero-length input string with an all-zero nonce and copy the tag to the return buffer.
|
|
byte* pbNonce = stackalloc byte[(int)NONCE_SIZE_IN_BYTES];
|
|
UnsafeBufferUtil.SecureZeroMemory(pbNonce, NONCE_SIZE_IN_BYTES);
|
|
DoGcmEncrypt(
|
|
pbKey: pbTempKeys,
|
|
cbKey: _symmetricAlgorithmSubkeyLengthInBytes,
|
|
pbNonce: pbNonce,
|
|
pbPlaintextData: &dummy,
|
|
cbPlaintextData: 0,
|
|
pbEncryptedData: &dummy,
|
|
pbTag: ptr);
|
|
}
|
|
|
|
ptr += TAG_SIZE_IN_BYTES;
|
|
CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length");
|
|
}
|
|
|
|
// retVal := { version || chainingMode || symAlgKeySize || nonceSize || symAlgBlockSize || symAlgTagSize || TAG-of-E("") }.
|
|
return retVal;
|
|
}
|
|
|
|
protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
|
|
{
|
|
// Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag
|
|
if (cbCiphertext < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)
|
|
{
|
|
throw Error.CryptCommon_PayloadInvalid();
|
|
}
|
|
|
|
// Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag }
|
|
|
|
uint cbPlaintext = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES));
|
|
|
|
byte[] retVal = new byte[cbPlaintext];
|
|
fixed (byte* pbRetVal = retVal)
|
|
{
|
|
// Calculate offsets
|
|
byte* pbKeyModifier = pbCiphertext;
|
|
byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
|
|
byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES];
|
|
byte* pbAuthTag = &pbEncryptedData[cbPlaintext];
|
|
|
|
// Use the KDF to recreate the symmetric block cipher key
|
|
// We'll need a temporary buffer to hold the symmetric encryption subkey
|
|
byte* pbSymmetricDecryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)];
|
|
try
|
|
{
|
|
_sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
|
|
pbLabel: pbAdditionalAuthenticatedData,
|
|
cbLabel: cbAdditionalAuthenticatedData,
|
|
contextHeader: _contextHeader,
|
|
pbContext: pbKeyModifier,
|
|
cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
|
|
pbDerivedKey: pbSymmetricDecryptionSubkey,
|
|
cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);
|
|
|
|
// Perform the decryption operation
|
|
|
|
using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
|
|
{
|
|
byte dummy;
|
|
byte* pbPlaintext = (pbRetVal != null) ? pbRetVal : &dummy; // CLR doesn't like pinning empty buffers
|
|
|
|
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;
|
|
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authInfo);
|
|
authInfo.pbNonce = pbNonce;
|
|
authInfo.cbNonce = NONCE_SIZE_IN_BYTES;
|
|
authInfo.pbTag = pbAuthTag;
|
|
authInfo.cbTag = TAG_SIZE_IN_BYTES;
|
|
|
|
// The call to BCryptDecrypt will also validate the authentication tag
|
|
uint cbDecryptedBytesWritten;
|
|
int ntstatus = UnsafeNativeMethods.BCryptDecrypt(
|
|
hKey: decryptionSubkeyHandle,
|
|
pbInput: pbEncryptedData,
|
|
cbInput: cbPlaintext,
|
|
pPaddingInfo: &authInfo,
|
|
pbIV: null, // IV not used; nonce provided in pPaddingInfo
|
|
cbIV: 0,
|
|
pbOutput: pbPlaintext,
|
|
cbOutput: cbPlaintext,
|
|
pcbResult: out cbDecryptedBytesWritten,
|
|
dwFlags: 0);
|
|
UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
|
|
CryptoUtil.Assert(cbDecryptedBytesWritten == cbPlaintext, "cbDecryptedBytesWritten == cbPlaintext");
|
|
|
|
// At this point, retVal := { decryptedPayload }
|
|
// And we're done!
|
|
return retVal;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// The buffer contains key material, so delete it.
|
|
UnsafeBufferUtil.SecureZeroMemory(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
_sp800_108_ctr_hmac_provider.Dispose();
|
|
|
|
// We don't want to dispose of the underlying algorithm instances because they
|
|
// might be reused.
|
|
}
|
|
|
|
// 'pbNonce' must point to a 96-bit buffer.
|
|
// 'pbTag' must point to a 128-bit buffer.
|
|
// 'pbEncryptedData' must point to a buffer the same length as 'pbPlaintextData'.
|
|
private void DoGcmEncrypt(byte* pbKey, uint cbKey, byte* pbNonce, byte* pbPlaintextData, uint cbPlaintextData, byte* pbEncryptedData, byte* pbTag)
|
|
{
|
|
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authCipherInfo;
|
|
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authCipherInfo);
|
|
authCipherInfo.pbNonce = pbNonce;
|
|
authCipherInfo.cbNonce = NONCE_SIZE_IN_BYTES;
|
|
authCipherInfo.pbTag = pbTag;
|
|
authCipherInfo.cbTag = TAG_SIZE_IN_BYTES;
|
|
|
|
using (var keyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbKey, cbKey))
|
|
{
|
|
uint cbResult;
|
|
int ntstatus = UnsafeNativeMethods.BCryptEncrypt(
|
|
hKey: keyHandle,
|
|
pbInput: pbPlaintextData,
|
|
cbInput: cbPlaintextData,
|
|
pPaddingInfo: &authCipherInfo,
|
|
pbIV: null,
|
|
cbIV: 0,
|
|
pbOutput: pbEncryptedData,
|
|
cbOutput: cbPlaintextData,
|
|
pcbResult: out cbResult,
|
|
dwFlags: 0);
|
|
UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
|
|
CryptoUtil.Assert(cbResult == cbPlaintextData, "cbResult == cbPlaintextData");
|
|
}
|
|
}
|
|
|
|
protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
|
|
{
|
|
// Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag.
|
|
// In GCM, the encrypted output will be the same length as the plaintext input.
|
|
byte[] retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + cbPlaintext + TAG_SIZE_IN_BYTES + cbPostBuffer)];
|
|
fixed (byte* pbRetVal = retVal)
|
|
{
|
|
// Calculate offsets
|
|
byte* pbKeyModifier = &pbRetVal[cbPreBuffer];
|
|
byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
|
|
byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES];
|
|
byte* pbAuthTag = &pbEncryptedData[cbPlaintext];
|
|
|
|
// Randomly generate the key modifier and nonce
|
|
_genRandom.GenRandom(pbKeyModifier, KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES);
|
|
|
|
// At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer }
|
|
|
|
// Use the KDF to generate a new symmetric block cipher key
|
|
// We'll need a temporary buffer to hold the symmetric encryption subkey
|
|
byte* pbSymmetricEncryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)];
|
|
try
|
|
{
|
|
_sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
|
|
pbLabel: pbAdditionalAuthenticatedData,
|
|
cbLabel: cbAdditionalAuthenticatedData,
|
|
contextHeader: _contextHeader,
|
|
pbContext: pbKeyModifier,
|
|
cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
|
|
pbDerivedKey: pbSymmetricEncryptionSubkey,
|
|
cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);
|
|
|
|
// Perform the encryption operation
|
|
DoGcmEncrypt(
|
|
pbKey: pbSymmetricEncryptionSubkey,
|
|
cbKey: _symmetricAlgorithmSubkeyLengthInBytes,
|
|
pbNonce: pbNonce,
|
|
pbPlaintextData: pbPlaintext,
|
|
cbPlaintextData: cbPlaintext,
|
|
pbEncryptedData: pbEncryptedData,
|
|
pbTag: pbAuthTag);
|
|
|
|
// At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer }
|
|
// And we're done!
|
|
return retVal;
|
|
}
|
|
finally
|
|
{
|
|
// The buffer contains key material, so delete it.
|
|
UnsafeBufferUtil.SecureZeroMemory(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|