Skip to content

Commit 8d49973

Browse files
committed
simplify tests and cbc
1 parent a4e3ca4 commit 8d49973

File tree

4 files changed

+154
-27
lines changed

4 files changed

+154
-27
lines changed

src/DataProtection/DataProtection/src/Cng/CbcAuthenticatedEncryptor.cs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,99 @@ public override int GetEncryptedSize(int plainTextLength, uint preBufferSize, ui
308308

309309
public override bool TryEncrypt(ReadOnlySpan<byte> plainText, ReadOnlySpan<byte> additionalAuthenticatedData, Span<byte> destination, out int bytesWritten)
310310
{
311-
throw new NotImplementedException();
311+
bytesWritten = 0;
312+
313+
try
314+
{
315+
// This buffer will be used to hold the symmetric encryption and HMAC subkeys
316+
// used in the generation of this payload.
317+
var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes);
318+
byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)];
319+
320+
try
321+
{
322+
// Randomly generate the key modifier and IV.
323+
var cbKeyModifierAndIV = checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes);
324+
byte* pbKeyModifierAndIV = stackalloc byte[checked((int)cbKeyModifierAndIV)];
325+
_genRandom.GenRandom(pbKeyModifierAndIV, cbKeyModifierAndIV);
326+
327+
// Calculate offsets
328+
byte* pbKeyModifier = pbKeyModifierAndIV;
329+
byte* pbIV = &pbKeyModifierAndIV[KEY_MODIFIER_SIZE_IN_BYTES];
330+
331+
// Use the KDF to generate a new symmetric encryption and HMAC subkey
332+
fixed (byte* pbAdditionalAuthenticatedData = additionalAuthenticatedData)
333+
{
334+
_sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
335+
pbLabel: pbAdditionalAuthenticatedData,
336+
cbLabel: (uint)additionalAuthenticatedData.Length,
337+
contextHeader: _contextHeader,
338+
pbContext: pbKeyModifier,
339+
cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
340+
pbDerivedKey: pbTempSubkeys,
341+
cbDerivedKey: cbTempSubkeys);
342+
}
343+
344+
// Calculate offsets
345+
byte* pbSymmetricEncryptionSubkey = pbTempSubkeys;
346+
byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes];
347+
348+
using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
349+
{
350+
// Get the padded output size
351+
fixed (byte* pbPlainText = plainText)
352+
{
353+
var cbOutputCiphertext = GetCbcEncryptedOutputSizeWithPadding(symmetricKeyHandle, pbPlainText, (uint)plainText.Length);
354+
355+
fixed (byte* pbDestination = destination)
356+
{
357+
// Calculate offsets in destination
358+
byte* pbOutputKeyModifier = pbDestination;
359+
byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
360+
byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes];
361+
byte* pbOutputHmac = &pbOutputCiphertext[cbOutputCiphertext];
362+
363+
// Copy key modifier and IV to destination
364+
UnsafeBufferUtil.BlockCopy(from: pbKeyModifierAndIV, to: pbOutputKeyModifier, byteCount: cbKeyModifierAndIV);
365+
bytesWritten += checked((int)cbKeyModifierAndIV);
366+
367+
// Perform CBC encryption directly into destination
368+
DoCbcEncrypt(
369+
symmetricKeyHandle: symmetricKeyHandle,
370+
pbIV: pbIV,
371+
pbInput: pbPlainText,
372+
cbInput: (uint)plainText.Length,
373+
pbOutput: pbOutputCiphertext,
374+
cbOutput: cbOutputCiphertext);
375+
bytesWritten += checked((int)cbOutputCiphertext);
376+
377+
// Compute the HMAC over the IV and the ciphertext
378+
using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
379+
{
380+
hashHandle.HashData(
381+
pbInput: pbOutputIV,
382+
cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext),
383+
pbHashDigest: pbOutputHmac,
384+
cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
385+
}
386+
bytesWritten += checked((int)_hmacAlgorithmDigestLengthInBytes);
387+
388+
return true;
389+
}
390+
}
391+
}
392+
}
393+
finally
394+
{
395+
// Buffer contains sensitive material; delete it.
396+
UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
397+
}
398+
}
399+
catch (Exception ex) when (ex.RequiresHomogenization())
400+
{
401+
// Homogenize all exceptions to CryptographicException.
402+
throw Error.CryptCommon_GenericError(ex);
403+
}
312404
}
313405
#endif
314406

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Cng/CbcAuthenticatedEncryptorTests.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Security.Cryptography;
56
using System.Text;
67
using Microsoft.AspNetCore.Cryptography.Cng;
78
using Microsoft.AspNetCore.Cryptography.SafeHandles;
89
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
910
using Microsoft.AspNetCore.DataProtection.Test.Shared;
11+
using Microsoft.AspNetCore.DataProtection.Tests.Internal;
1012
using Microsoft.AspNetCore.InternalTesting;
1113

1214
namespace Microsoft.AspNetCore.DataProtection.Cng;
@@ -134,13 +136,6 @@ public void Roundtrip_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetr
134136
ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
135137
ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
136138

137-
var expectedSize = encryptor.GetEncryptedSize(plaintext.Count);
138-
139-
byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
140-
Assert.Equal(expectedSize, ciphertext.Length);
141-
142-
byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
143-
144-
Assert.Equal(plaintext.AsSpan(), decipheredtext.AsSpan());
139+
RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad);
145140
}
146141
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Collections.Generic;
7+
using System.Text;
8+
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
9+
10+
namespace Microsoft.AspNetCore.DataProtection.Tests.Internal;
11+
12+
internal static class RoundtripEncryptionHelpers
13+
{
14+
/// <summary>
15+
/// <see cref="IAuthenticatedEncryptor.TryEncrypt"/> and <see cref="IAuthenticatedEncryptor.TryDecrypt"/> APIs should do the same steps
16+
/// as <see cref="IAuthenticatedEncryptor.Encrypt"/> and <see cref="IAuthenticatedEncryptor.Decrypt"/> APIs.
17+
/// <br/>
18+
/// Method ensures that the two APIs are equivalent in terms of their behavior by performing a roundtrip encrypt-decrypt test.
19+
/// </summary>
20+
public static void AssertTryEncryptTryDecryptParity(IAuthenticatedEncryptor encryptor, ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> aad)
21+
=> AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad);
22+
23+
/// <summary>
24+
/// <see cref="IAuthenticatedEncryptor.TryEncrypt"/> and <see cref="IAuthenticatedEncryptor.TryDecrypt"/> APIs should do the same steps
25+
/// as <see cref="IAuthenticatedEncryptor.Encrypt"/> and <see cref="IAuthenticatedEncryptor.Decrypt"/> APIs.
26+
/// <br/>
27+
/// Method ensures that the two APIs are equivalent in terms of their behavior by performing a roundtrip encrypt-decrypt test.
28+
/// </summary>
29+
public static void AssertTryEncryptTryDecryptParity(IAuthenticatedEncryptor encryptor, ArraySegment<byte> plaintext, ArraySegment<byte> aad)
30+
{
31+
// assert "allocatey" Encrypt/Decrypt APIs roundtrip correctly
32+
byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
33+
byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
34+
Assert.Equal(plaintext.AsSpan(), decipheredtext.AsSpan());
35+
36+
// assert calculated size is correct
37+
var expectedSize = encryptor.GetEncryptedSize(plaintext.Count);
38+
Assert.Equal(expectedSize, ciphertext.Length);
39+
40+
// perform TryEncrypt and Decrypt roundtrip - ensures cross operation compatibility
41+
var cipherTextPooled = ArrayPool<byte>.Shared.Rent(expectedSize);
42+
try
43+
{
44+
var tryEncryptResult = encryptor.TryEncrypt(plaintext, aad, cipherTextPooled, out var bytesWritten);
45+
Assert.Equal(expectedSize, bytesWritten);
46+
Assert.True(tryEncryptResult);
47+
48+
var decipheredTryEncrypt = encryptor.Decrypt(new ArraySegment<byte>(cipherTextPooled, 0, expectedSize), aad);
49+
Assert.Equal(plaintext.AsSpan(), decipheredTryEncrypt.AsSpan());
50+
}
51+
finally
52+
{
53+
ArrayPool<byte>.Shared.Return(cipherTextPooled);
54+
}
55+
}
56+
}

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/Managed/ManagedAuthenticatedEncryptorTests.cs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Security.Cryptography;
66
using System.Text;
77
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
8+
using Microsoft.AspNetCore.DataProtection.Tests.Internal;
89

910
namespace Microsoft.AspNetCore.DataProtection.Managed;
1011

@@ -132,23 +133,6 @@ public void Roundtrip_TryEncryptDecrypt_CorrectlyEstimatesDataLength(int symmetr
132133
ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
133134
ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
134135

135-
var expectedSize = encryptor.GetEncryptedSize(plaintext.Count);
136-
var cipherTextPooled = ArrayPool<byte>.Shared.Rent(expectedSize);
137-
var tryEncryptResult = encryptor.TryEncrypt(plaintext, aad, cipherTextPooled, out var bytesWritten);
138-
Assert.Equal(expectedSize, bytesWritten);
139-
Assert.True(tryEncryptResult);
140-
141-
byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
142-
143-
// validate length of the cipherText is the same as pre-calculated size
144-
// and TryEncrypt produces the same result as Encrypt API
145-
Assert.Equal(expectedSize, ciphertext.Length);
146-
147-
byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
148-
var decipheredTextFromTryEncrypt = encryptor.Decrypt(new ArraySegment<byte>(cipherTextPooled, 0, expectedSize), aad);
149-
Assert.Equal(plaintext.AsSpan(), decipheredtext.AsSpan());
150-
Assert.Equal(plaintext.AsSpan(), decipheredTextFromTryEncrypt.AsSpan());
151-
152-
ArrayPool<byte>.Shared.Return(cipherTextPooled);
136+
RoundtripEncryptionHelpers.AssertTryEncryptTryDecryptParity(encryptor, plaintext, aad);
153137
}
154138
}

0 commit comments

Comments
 (0)