Make EphemeralDataProtectionProvider and ProtectedMemoryBlob work on non-Windows platforms.
This commit is contained in:
parent
7d5a29a9fd
commit
c3b76d14a3
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
/// Options for configuring an authenticated encryption mechanism which uses
|
/// Options for configuring an authenticated encryption mechanism which uses
|
||||||
/// Windows CNG algorithms in CBC encryption + HMAC validation modes.
|
/// Windows CNG algorithms in CBC encryption + HMAC validation modes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions
|
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the algorithm to use for symmetric encryption.
|
/// The name of the algorithm to use for symmetric encryption.
|
||||||
|
|
@ -178,5 +178,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
|
||||||
|
{
|
||||||
|
return CreateAuthenticatedEncryptor(secret);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
/// Options for configuring an authenticated encryption mechanism which uses
|
/// Options for configuring an authenticated encryption mechanism which uses
|
||||||
/// Windows CNG encryption algorithms in Galois/Counter Mode.
|
/// Windows CNG encryption algorithms in Galois/Counter Mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions
|
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the algorithm to use for symmetric encryption.
|
/// The name of the algorithm to use for symmetric encryption.
|
||||||
|
|
@ -120,5 +120,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
|
||||||
|
{
|
||||||
|
return CreateAuthenticatedEncryptor(secret);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
|
{
|
||||||
|
internal interface IInternalConfigurationOptions
|
||||||
|
{
|
||||||
|
IAuthenticatedEncryptor CreateAuthenticatedEncryptor(ISecret secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
/// Options for configuring an authenticated encryption mechanism which uses
|
/// Options for configuring an authenticated encryption mechanism which uses
|
||||||
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
|
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions
|
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of the algorithm to use for symmetric encryption.
|
/// The type of the algorithm to use for symmetric encryption.
|
||||||
|
|
@ -102,6 +102,11 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
|
||||||
return ((IActivator<KeyedHashAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
|
return ((IActivator<KeyedHashAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
|
||||||
|
{
|
||||||
|
return CreateAuthenticatedEncryptor(secret);
|
||||||
|
}
|
||||||
|
|
||||||
private interface IActivator<out T>
|
private interface IActivator<out T>
|
||||||
{
|
{
|
||||||
Func<T> Creator { get; }
|
Func<T> Creator { get; }
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
|
if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
|
||||||
{
|
{
|
||||||
// Fastest implementation: AES-GCM
|
// Fastest implementation: AES-GCM
|
||||||
keyringProvider = new CngEphemeralKeyRing();
|
keyringProvider = new EphemeralKeyRing<CngGcmAuthenticatedEncryptorConfigurationOptions>();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Slowest implementation: managed CBC + HMAC
|
// Slowest implementation: managed CBC + HMAC
|
||||||
keyringProvider = new ManagedEphemeralKeyRing();
|
keyringProvider = new EphemeralKeyRing<ManagedAuthenticatedEncryptorConfigurationOptions>();
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider);
|
_dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider);
|
||||||
|
|
@ -55,29 +55,13 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A special key ring that only understands one key id and which uses CNG.
|
private sealed class EphemeralKeyRing<T> : IKeyRing, IKeyRingProvider
|
||||||
private sealed class CngEphemeralKeyRing : IKeyRing, IKeyRingProvider
|
where T : IInternalConfigurationOptions, new()
|
||||||
{
|
{
|
||||||
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new CngGcmAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor<CngGcmAuthenticatedEncryptorConfigurationOptions>()).CreateNewConfiguration().CreateEncryptorInstance();
|
// Currently hardcoded to a 512-bit KDK.
|
||||||
|
private const int NUM_BYTES_IN_KDK = 512 / 8;
|
||||||
|
|
||||||
public Guid DefaultKeyId { get; } = default(Guid);
|
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().CreateAuthenticatedEncryptor(ProtectedMemoryBlob.Random(NUM_BYTES_IN_KDK));
|
||||||
|
|
||||||
public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked)
|
|
||||||
{
|
|
||||||
isRevoked = false;
|
|
||||||
return (keyId == default(Guid)) ? DefaultAuthenticatedEncryptor : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IKeyRing GetCurrentKeyRing()
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A special key ring that only understands one key id and which uses managed CBC + HMAC.
|
|
||||||
private sealed class ManagedEphemeralKeyRing : IKeyRing, IKeyRingProvider
|
|
||||||
{
|
|
||||||
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new ManagedAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor<ManagedAuthenticatedEncryptorConfigurationOptions>()).CreateNewConfiguration().CreateEncryptorInstance();
|
|
||||||
|
|
||||||
public Guid DefaultKeyId { get; } = default(Guid);
|
public Guid DefaultKeyId { get; } = default(Guid);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNet.Security.DataProtection.Cng;
|
using Microsoft.AspNet.Security.DataProtection.Cng;
|
||||||
|
using Microsoft.AspNet.Security.DataProtection.Managed;
|
||||||
using Microsoft.AspNet.Security.DataProtection.SafeHandles;
|
using Microsoft.AspNet.Security.DataProtection.SafeHandles;
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Security.DataProtection
|
namespace Microsoft.AspNet.Security.DataProtection
|
||||||
{
|
{
|
||||||
|
|
@ -13,14 +13,14 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
// from wincrypt.h
|
// from wincrypt.h
|
||||||
private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16;
|
private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16;
|
||||||
|
|
||||||
private readonly SecureLocalAllocHandle _encryptedMemoryHandle;
|
private readonly SecureLocalAllocHandle _localAllocHandle;
|
||||||
private readonly uint _plaintextLength;
|
private readonly uint _plaintextLength;
|
||||||
|
|
||||||
public ProtectedMemoryBlob(ArraySegment<byte> plaintext)
|
public ProtectedMemoryBlob(ArraySegment<byte> plaintext)
|
||||||
{
|
{
|
||||||
plaintext.Validate();
|
plaintext.Validate();
|
||||||
|
|
||||||
_encryptedMemoryHandle = Protect(plaintext);
|
_localAllocHandle = Protect(plaintext);
|
||||||
_plaintextLength = (uint)plaintext.Count;
|
_plaintextLength = (uint)plaintext.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
throw new ArgumentOutOfRangeException("plaintextLength");
|
throw new ArgumentOutOfRangeException("plaintextLength");
|
||||||
}
|
}
|
||||||
|
|
||||||
_encryptedMemoryHandle = Protect(plaintext, (uint)plaintextLength);
|
_localAllocHandle = Protect(plaintext, (uint)plaintextLength);
|
||||||
_plaintextLength = (uint)plaintextLength;
|
_plaintextLength = (uint)plaintextLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
if (other != null)
|
if (other != null)
|
||||||
{
|
{
|
||||||
// Fast-track: simple deep copy scenario.
|
// Fast-track: simple deep copy scenario.
|
||||||
this._encryptedMemoryHandle = other._encryptedMemoryHandle.Duplicate();
|
this._localAllocHandle = other._localAllocHandle.Duplicate();
|
||||||
this._plaintextLength = other._plaintextLength;
|
this._plaintextLength = other._plaintextLength;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -68,7 +68,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer));
|
secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer));
|
||||||
_encryptedMemoryHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
|
_localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
|
||||||
_plaintextLength = (uint)tempPlaintextBuffer.Length;
|
_plaintextLength = (uint)tempPlaintextBuffer.Length;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_encryptedMemoryHandle.Dispose();
|
_localAllocHandle.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext)
|
private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext)
|
||||||
|
|
@ -102,6 +102,16 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
|
|
||||||
private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext)
|
private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext)
|
||||||
{
|
{
|
||||||
|
// If we're not running on a platform that supports CryptProtectMemory,
|
||||||
|
// shove the plaintext directly into a LocalAlloc handle. Ideally we'd
|
||||||
|
// mark this memory page as non-pageable, but this is fraught with peril.
|
||||||
|
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
|
||||||
|
{
|
||||||
|
SecureLocalAllocHandle handle = SecureLocalAllocHandle.Allocate((IntPtr)checked((int)cbPlaintext));
|
||||||
|
UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: handle, byteCount: cbPlaintext);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
// We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE.
|
// We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE.
|
||||||
uint numTotalBytesToAllocate = cbPlaintext;
|
uint numTotalBytesToAllocate = cbPlaintext;
|
||||||
uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE);
|
uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE);
|
||||||
|
|
@ -135,6 +145,12 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Don't use CNG if we're not on Windows.
|
||||||
|
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
|
||||||
|
{
|
||||||
|
return new ProtectedMemoryBlob(ManagedGenRandomImpl.Instance.GenRandom(numBytes));
|
||||||
|
}
|
||||||
|
|
||||||
byte[] bytes = new byte[numBytes];
|
byte[] bytes = new byte[numBytes];
|
||||||
fixed (byte* pbBytes = bytes)
|
fixed (byte* pbBytes = bytes)
|
||||||
{
|
{
|
||||||
|
|
@ -153,18 +169,26 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
|
|
||||||
private void UnprotectInto(byte* pbBuffer)
|
private void UnprotectInto(byte* pbBuffer)
|
||||||
{
|
{
|
||||||
|
// If we're not running on a platform that supports CryptProtectMemory,
|
||||||
|
// the handle contains plaintext bytes.
|
||||||
|
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
|
||||||
|
{
|
||||||
|
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0)
|
if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0)
|
||||||
{
|
{
|
||||||
// Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there.
|
// Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there.
|
||||||
// We go through this code path even for empty plaintexts since we still want SafeHandle dispose semantics.
|
// We go through this code path even for empty plaintexts since we still want SafeHandle dispose semantics.
|
||||||
UnsafeBufferUtil.BlockCopy(from: _encryptedMemoryHandle, to: pbBuffer, byteCount: _plaintextLength);
|
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
|
||||||
MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
|
MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and
|
// Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and
|
||||||
// perform the decryption in the duplicate buffer, then copy the plaintext data over.
|
// perform the decryption in the duplicate buffer, then copy the plaintext data over.
|
||||||
using (var duplicateHandle = _encryptedMemoryHandle.Duplicate())
|
using (var duplicateHandle = _localAllocHandle.Duplicate())
|
||||||
{
|
{
|
||||||
MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length));
|
MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length));
|
||||||
UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength);
|
UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
|
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
|
||||||
{
|
{
|
||||||
public unsafe class CbcAuthenticatedEncryptorTests
|
public class CbcAuthenticatedEncryptorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Encrypt_Decrypt_RoundTrips()
|
public void Encrypt_Decrypt_RoundTrips()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
|
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
|
||||||
{
|
{
|
||||||
public unsafe class GcmAuthenticatedEncryptorTests
|
public class GcmAuthenticatedEncryptorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Encrypt_Decrypt_RoundTrips()
|
public void Encrypt_Decrypt_RoundTrips()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Security.DataProtection.Test
|
||||||
|
{
|
||||||
|
public class EphemeralDataProtectionProviderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DifferentProvider_SamePurpose_DoesNotRoundTripData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dataProtector1 = new EphemeralDataProtectionProvider().CreateProtector("purpose");
|
||||||
|
var dataProtector2 = new EphemeralDataProtectionProvider().CreateProtector("purpose");
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
// Each instance of the EphemeralDataProtectionProvider has its own unique KDK, so payloads can't be shared.
|
||||||
|
byte[] protectedBytes = dataProtector1.Protect(bytes);
|
||||||
|
Assert.ThrowsAny<CryptographicException>(() =>
|
||||||
|
{
|
||||||
|
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleProvider_DifferentPurpose_DoesNotRoundTripData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dataProtectionProvider = new EphemeralDataProtectionProvider();
|
||||||
|
var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
|
||||||
|
var dataProtector2 = dataProtectionProvider.CreateProtector("different purpose");
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
byte[] protectedBytes = dataProtector1.Protect(bytes);
|
||||||
|
Assert.ThrowsAny<CryptographicException>(() =>
|
||||||
|
{
|
||||||
|
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleProvider_SamePurpose_RoundTripsData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dataProtectionProvider = new EphemeralDataProtectionProvider();
|
||||||
|
var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
|
||||||
|
var dataProtector2 = dataProtectionProvider.CreateProtector("purpose"); // should be equivalent to the previous instance
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
byte[] protectedBytes = dataProtector1.Protect(bytes);
|
||||||
|
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(bytes, unprotectedBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue