Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65aa435
Fun with spans and pointers
HaoK Feb 1, 2021
4a5d224
Merge branch 'main' into haok/gcm
HaoK Feb 1, 2021
d658fcc
Update Version.Details.xml
HaoK Feb 1, 2021
4b43975
Cleanup
HaoK Feb 1, 2021
0c6d717
Flip check order
HaoK Feb 1, 2021
4ae2bdb
Update Versions.props
HaoK Feb 2, 2021
fba0a99
Cleanup move decls into unsafe block
HaoK Feb 2, 2021
b97ecc0
Update overload
HaoK Feb 2, 2021
2f76630
Update Version.Details.xml
HaoK Feb 2, 2021
b81c9d4
Update Versions.props
HaoK Feb 2, 2021
717e303
Update src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
HaoK Feb 2, 2021
d9f33b9
Update eng/Dependencies.props
HaoK Feb 2, 2021
b1a59e8
Update CryptoUtil.cs
HaoK Feb 2, 2021
37e4aa2
Renames and add managed aesgcm encryptor
HaoK Feb 5, 2021
98ac949
Cleanup
HaoK Feb 5, 2021
a85582c
Fixup
HaoK Feb 5, 2021
052296f
Update CngGcmAuthenticatedEncryptor.cs
HaoK Feb 5, 2021
7a932de
Fix bugs
HaoK Feb 8, 2021
47dcec9
Fix typo in context header :(
HaoK Feb 8, 2021
53e3d8f
Cleanup
HaoK Feb 8, 2021
512a9f6
Revert print buffer
HaoK Feb 8, 2021
fa4ee92
Cleanup
HaoK Feb 8, 2021
bc1194d
Code review feedback
HaoK Feb 24, 2021
c1f8809
Update AuthenticatedEncryptorFactory.cs
HaoK Feb 24, 2021
6a866f4
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
68f37c2
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
a631a30
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
4e84ecf
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
96b1b4b
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
fb78d32
Update AuthenticatedEncryptorDescriptorTests.cs
HaoK Feb 24, 2021
75a1e0f
Update AesGcmAuthenticatedEncryptor.cs
HaoK Feb 24, 2021
a752598
Update src/DataProtection/DataProtection/src/Managed/AesGcmAuthentica…
HaoK Feb 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/DataProtection/Cryptography.Internal/src/CryptoUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,19 @@ public static T Fail<T>(string message) where T : class
#endif
public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint count)
{
#if NETCOREAPP
var byteCount = Convert.ToInt32(count);
var bytesA = new ReadOnlySpan<byte>(bufA, byteCount);
var bytesB = new ReadOnlySpan<byte>(bufB, byteCount);
return CryptographicOperations.FixedTimeEquals(bytesA, bytesB);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

There are a bunch of places in data protection where we could leverage new primitives introduced to the framework in the past few releases. Might be interesting to have an issue for that. Identity likely can also benefit from it.

#else
bool areEqual = true;
for (uint i = 0; i < count; i++)
{
areEqual &= (bufA[i] == bufB[i]);
}
return areEqual;
#endif
}

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
Expand All @@ -91,12 +98,21 @@ public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int cou
// An error at the call site isn't usable for timing attacks.
Assert(countA == countB, "countA == countB");

#if NETCOREAPP
unsafe
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extreme nits: No need to use unsafe here. If desired, you can also put the NoInlining | NoOptimization attribute within an #if !NETCOREAPP block. Maybe those can come with the spanification PR so as not to cause noise here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this in the span PR

{
return CryptographicOperations.FixedTimeEquals(
bufA.AsSpan(start: offsetA, length: countA),
bufB.AsSpan(start: offsetB, length: countB));
}
#else
bool areEqual = true;
for (int i = 0; i < countA; i++)
{
areEqual &= (bufA[offsetA + i] == bufB[offsetB + i]);
}
return areEqual;
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Managed;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
Expand Down Expand Up @@ -54,6 +55,9 @@ public AuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)

if (IsGcmAlgorithm(authenticatedConfiguration.EncryptionAlgorithm))
{
#if NETCOREAPP
return new AesGcmAuthenticatedEncryptor(secret, GetAlgorithmKeySizeInBits(authenticatedConfiguration.EncryptionAlgorithm) / 8);
#else
// GCM requires CNG, and CNG is only supported on Windows.
if (!OSVersionUtil.IsWindows())
{
Expand All @@ -69,6 +73,7 @@ public AuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
};

return new CngGcmAuthenticatedEncryptorFactory(_loggerFactory).CreateAuthenticatedEncryptorInstance(secret, configuration);
#endif
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
{
/// <summary>
/// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="GcmAuthenticatedEncryptor"/>.
/// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="CngGcmAuthenticatedEncryptor"/>.
/// </summary>
public sealed class CngGcmAuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
{
Expand Down Expand Up @@ -48,7 +48,7 @@ public CngGcmAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)

[SupportedOSPlatform("windows")]
[return: NotNullIfNotNull("configuration")]
internal GcmAuthenticatedEncryptor? CreateAuthenticatedEncryptorInstance(
internal CngGcmAuthenticatedEncryptor? CreateAuthenticatedEncryptorInstance(
ISecret secret,
CngGcmAuthenticatedEncryptorConfiguration configuration)
{
Expand All @@ -57,7 +57,7 @@ public CngGcmAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
return null;
}

return new GcmAuthenticatedEncryptor(
return new CngGcmAuthenticatedEncryptor(
keyDerivationKey: new Secret(secret),
symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(configuration),
symmetricAlgorithmKeySizeInBytes: (uint)(configuration.EncryptionAlgorithmKeySize / 8));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.AspNetCore.Cryptography;
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.Cryptography.SafeHandles;
Expand All @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.DataProtection.Cng
// 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
internal unsafe sealed class CngGcmAuthenticatedEncryptor : 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
Expand All @@ -38,12 +37,12 @@ internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncrypt
private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;

public GcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom? genRandom = null)
public CngGcmAuthenticatedEncryptor(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;
Expand Down Expand Up @@ -151,7 +150,6 @@ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byt
cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);

// Perform the decryption operation

using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
{
byte dummy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// 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.

#if NETCOREAPP
using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.SP800_108;

namespace Microsoft.AspNetCore.DataProtection.Managed
{
// An encryptor that uses AesGcm to do encryption
internal unsafe sealed class AesGcmAuthenticatedEncryptor : IOptimizedAuthenticatedEncryptor, IDisposable
{
// 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
// on CngGcmAuthenticatedEncryptor for how this is used to overcome GCM's IV limitations.
private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;

private const int NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV
private const int TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size

// See CngGcmAuthenticatedEncryptor.CreateContextHeader for how these were precomputed

// 128 "00-01-00-00-00-10-00-00-00-0C-00-00-00-10-00-00-00-10-95-7C-50-FF-69-2E-38-8B-9A-D5-C7-68-9E-4B-9E-2B"
private static readonly byte[] AES_128_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x95, 0x7C, 0x50, 0xFF, 0x69, 0x2E, 0x38, 0x8B, 0x9A, 0xD5, 0xC7, 0x68, 0x9E, 0x4B, 0x9E, 0x2B };

// 192 "00-01-00-00-00-18-00-00-00-0C-00-00-00-10-00-00-00-10-0D-AA-01-3A-95-0A-DA-2B-79-8F-5F-F2-72-FA-D3-63"
private static readonly byte[] AES_192_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x0D, 0xAA, 0x01, 0x3A, 0x95, 0x0A, 0xDA, 0x2B, 0x79, 0x8F, 0x5F, 0xF2, 0x72, 0xFA, 0xD3, 0x63 };

// 256 00-01-00-00-00-20-00-00-00-0C-00-00-00-10-00-00-00-10-E7-DC-CE-66-DF-85-5A-32-3A-6B-B7-BD-7A-59-BE-45
private static readonly byte[] AES_256_GCM_Header = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0xE7, 0xDC, 0xCE, 0x66, 0xDF, 0x85, 0x5A, 0x32, 0x3A, 0x6B, 0xB7, 0xBD, 0x7A, 0x59, 0xBE, 0x45 };

private static readonly Func<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512

private readonly byte[] _contextHeader;

private readonly Secret _keyDerivationKey;
private readonly int _derivedkeySizeInBytes;
private readonly IManagedGenRandom _genRandom;

public AesGcmAuthenticatedEncryptor(ISecret keyDerivationKey, int derivedKeySizeInBytes, IManagedGenRandom? genRandom = null)
{
_keyDerivationKey = new Secret(keyDerivationKey);
_derivedkeySizeInBytes = derivedKeySizeInBytes;

switch (_derivedkeySizeInBytes)
{
case 16:
_contextHeader = AES_128_GCM_Header;
break;
case 24:
_contextHeader = AES_192_GCM_Header;
break;
case 32:
_contextHeader = AES_256_GCM_Header;
break;
default:
throw CryptoUtil.Fail("Unexpected AES key size in bytes only support 16, 24, 32."); // should never happen
}

_genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
}

public byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
{
ciphertext.Validate();
additionalAuthenticatedData.Validate();

// Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag
if (ciphertext.Count < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)
{
throw Error.CryptCommon_PayloadInvalid();
}

// Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag }
var plaintextBytes = ciphertext.Count - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES);
var plaintext = new byte[plaintextBytes];

try
{
// Step 1: Extract the key modifier from the payload.

int keyModifierOffset; // position in ciphertext.Array where key modifier begins
int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins
int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins
int tagOffset; // position in ciphertext.Array where encrypted data ends

checked
{
keyModifierOffset = ciphertext.Offset;
nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES;
tagOffset = encryptedDataOffset + plaintextBytes;
}

var keyModifier = new ArraySegment<byte>(ciphertext.Array!, keyModifierOffset, KEY_MODIFIER_SIZE_IN_BYTES);

// Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys.
// We pin all unencrypted keys to limit their exposure via GC relocation.

var decryptedKdk = new byte[_keyDerivationKey.Length];
var derivedKey = new byte[_derivedkeySizeInBytes];

fixed (byte* __unused__1 = decryptedKdk)
fixed (byte* __unused__2 = derivedKey)
{
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
context: keyModifier,
prfFactory: _kdkPrfFactory,
output: new ArraySegment<byte>(derivedKey));

// Perform the decryption operation
var nonce = new Span<byte>(ciphertext.Array, nonceOffset, NONCE_SIZE_IN_BYTES);
var tag = new Span<byte>(ciphertext.Array, tagOffset, TAG_SIZE_IN_BYTES);
var encrypted = new Span<byte>(ciphertext.Array, encryptedDataOffset, plaintextBytes);
using var aes = new AesGcm(derivedKey);
aes.Decrypt(nonce, encrypted, tag, plaintext);
return plaintext;
}
finally
{
// delete since these contain secret material
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
Array.Clear(derivedKey, 0, derivedKey.Length);
}
}
}
catch (Exception ex) when (ex.RequiresHomogenization())
{
// Homogenize all exceptions to CryptographicException.
throw Error.CryptCommon_GenericError(ex);
}
}

public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
{
plaintext.Validate();
additionalAuthenticatedData.Validate();

try
{
// 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.
var retVal = new byte[checked(preBufferSize + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + plaintext.Count + TAG_SIZE_IN_BYTES + postBufferSize)];
int keyModifierOffset; // position in ciphertext.Array where key modifier begins
int nonceOffset; // position in ciphertext.Array where key modifier ends / nonce begins
int encryptedDataOffset; // position in ciphertext.Array where nonce ends / encryptedData begins
int tagOffset; // position in ciphertext.Array where encrypted data ends

checked
{
keyModifierOffset = plaintext.Offset + (int)preBufferSize;
nonceOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
encryptedDataOffset = nonceOffset + NONCE_SIZE_IN_BYTES;
tagOffset = encryptedDataOffset + plaintext.Count;
}

// Randomly generate the key modifier and nonce
var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES);
var nonceBytes = _genRandom.GenRandom(NONCE_SIZE_IN_BYTES);

Buffer.BlockCopy(keyModifier, 0, retVal, (int)preBufferSize, keyModifier.Length);
Buffer.BlockCopy(nonceBytes, 0, retVal, (int)preBufferSize + keyModifier.Length, nonceBytes.Length);

// 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
var decryptedKdk = new byte[_keyDerivationKey.Length];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GrabYourPitchforks would it be unwise to use the shared ArrayPool here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to merge this as is, can make this switch in the follow up / spanification PR if needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, because it's a cryptographic secret. Within the runtime, our crypto libraries use an internal CryptoPool type instead of the public singleton ArrayPool<T>.Shared instance.

During the spanification work, this can be switched to using stackalloc (the common case) or the pinned object heap. Then no pooling will be needed at all.

var derivedKey = new byte[_derivedkeySizeInBytes];
fixed (byte* __unused__1 = decryptedKdk)
fixed (byte* __unused__2 = derivedKey)
{
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
context: keyModifier,
prfFactory: _kdkPrfFactory,
output: new ArraySegment<byte>(derivedKey));

// do gcm
var nonce = new Span<byte>(retVal, nonceOffset, NONCE_SIZE_IN_BYTES);
var tag = new Span<byte>(retVal, tagOffset, TAG_SIZE_IN_BYTES);
var encrypted = new Span<byte>(retVal, encryptedDataOffset, plaintext.Count);
using var aes = new AesGcm(derivedKey);
aes.Encrypt(nonce, plaintext, encrypted, tag);

// At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer }
// And we're done!
return retVal;
}
finally
{
// delete since these contain secret material
Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
Array.Clear(derivedKey, 0, derivedKey.Length);
}
}
}
catch (Exception ex) when (ex.RequiresHomogenization())
{
// Homogenize all exceptions to CryptographicException.
throw Error.CryptCommon_GenericError(ex);
}
}

public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
=> Encrypt(plaintext, additionalAuthenticatedData, 0, 0);

public void Dispose()
{
_keyDerivationKey.Dispose();
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void CreateEncrptorInstance_ExpectedDescriptorType_ReturnsEncryptor()

// Assert
Assert.NotNull(encryptor);
Assert.IsType<GcmAuthenticatedEncryptor>(encryptor);
Assert.IsType<CngGcmAuthenticatedEncryptor>(encryptor);
}
}
}
Loading