236 lines
8.5 KiB
C#
236 lines
8.5 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
namespace CleanArc.Infrastructure.Identity.Identity.DataProtection;
|
|
|
|
public class LookupProtector : ILookupProtector
|
|
{
|
|
private readonly ProtectorAlgorithm _defaultAlgorithm = ProtectorAlgorithm.Aes256Hmac512;
|
|
private readonly ILookupProtectorKeyRing _keyRing;
|
|
|
|
public LookupProtector(ILookupProtectorKeyRing keyRing)
|
|
{
|
|
_keyRing = keyRing;
|
|
}
|
|
|
|
public string Protect(string keyId, string data)
|
|
{
|
|
// Get the default algorithms.
|
|
// We does this so we can embed the algorithm details used in the cipher text so we can
|
|
// change algorithms as yet another collision appears in a hashing algorithm.
|
|
// See https://media.blackhat.com/bh-us-10/whitepapers/Sullivan/BlackHat-USA-2010-Sullivan-Cryptographic-Agility-wp.pdf
|
|
ProtectorAlgorithmHelper.GetAlgorithms(
|
|
_defaultAlgorithm,
|
|
out SymmetricAlgorithm encryptingAlgorithm,
|
|
out KeyedHashAlgorithm signingAlgorithm,
|
|
out int keyDerivationIterationCount);
|
|
|
|
var masterKey = MasterKey(keyId);
|
|
|
|
// Convert the string to bytes, because encryption works on bytes, not strings.
|
|
var plainText = Encoding.UTF8.GetBytes(data);
|
|
byte[] cipherTextAndIV;
|
|
|
|
// Derive a key for encryption from the master key
|
|
encryptingAlgorithm.Key = DerivedEncryptionKey(
|
|
masterKey,
|
|
encryptingAlgorithm,
|
|
keyDerivationIterationCount);
|
|
|
|
// As we need this to be deterministic, we need to force an IV that is derived from the plain text.
|
|
encryptingAlgorithm.IV = DerivedInitializationVector(
|
|
masterKey,
|
|
data,
|
|
encryptingAlgorithm,
|
|
keyDerivationIterationCount);
|
|
|
|
// And encrypt
|
|
using (var ms = new MemoryStream())
|
|
using (var cs = new CryptoStream(
|
|
ms,
|
|
encryptingAlgorithm.CreateEncryptor(),
|
|
CryptoStreamMode.Write))
|
|
{
|
|
cs.Write(plainText);
|
|
cs.FlushFinalBlock();
|
|
var encryptedData = ms.ToArray();
|
|
|
|
cipherTextAndIV = CombineByteArrays(encryptingAlgorithm.IV, encryptedData);
|
|
}
|
|
|
|
// Now get a signature for the data so we can detect tampering in situ.
|
|
byte[] signature = SignData(
|
|
cipherTextAndIV,
|
|
masterKey,
|
|
encryptingAlgorithm,
|
|
signingAlgorithm,
|
|
keyDerivationIterationCount);
|
|
|
|
// Add the signature to the cipher text.
|
|
var signedData = CombineByteArrays(signature, cipherTextAndIV);
|
|
|
|
// Add our algorithm identifier to the combined signature and cipher text.
|
|
var algorithmIdentifier = BitConverter.GetBytes((int)_defaultAlgorithm);
|
|
byte[] output = CombineByteArrays(algorithmIdentifier, signedData);
|
|
|
|
// Clean everything up.
|
|
encryptingAlgorithm.Clear();
|
|
signingAlgorithm.Clear();
|
|
encryptingAlgorithm.Dispose();
|
|
signingAlgorithm.Dispose();
|
|
|
|
Array.Clear(plainText, 0, plainText.Length);
|
|
|
|
// Return the results as a string.
|
|
return Convert.ToBase64String(output);
|
|
}
|
|
|
|
public string Unprotect(string keyId, string data)
|
|
{
|
|
var masterKey = MasterKey(keyId);
|
|
byte[] plainText;
|
|
|
|
// Take our string and convert it back to bytes.
|
|
var payload = Convert.FromBase64String(data);
|
|
|
|
// Read the saved algorithm details and create instances of those algorithms.
|
|
byte[] algorithmIdentifierAsBytes = new byte[4];
|
|
Buffer.BlockCopy(payload, 0, algorithmIdentifierAsBytes, 0, 4);
|
|
var algorithmIdentifier = (ProtectorAlgorithm)(BitConverter.ToInt32(algorithmIdentifierAsBytes, 0));
|
|
ProtectorAlgorithmHelper.GetAlgorithms(
|
|
_defaultAlgorithm,
|
|
out SymmetricAlgorithm encryptingAlgorithm,
|
|
out KeyedHashAlgorithm signingAlgorithm,
|
|
out int keyDerivationIterationCount);
|
|
|
|
// Now extract the signature
|
|
byte[] signature = new byte[signingAlgorithm.HashSize / 8];
|
|
Buffer.BlockCopy(payload, 4, signature, 0, signingAlgorithm.HashSize / 8);
|
|
|
|
// And finally grab the rest of the data
|
|
var dataLength = payload.Length - 4 - signature.Length;
|
|
byte[] cipherTextAndIV = new byte[dataLength];
|
|
Buffer.BlockCopy(payload, 4 + signature.Length, cipherTextAndIV, 0, dataLength);
|
|
|
|
// Check the signature before anything else is done to detect tampering and avoid
|
|
// oracles.
|
|
byte[] computedSignature = SignData(
|
|
cipherTextAndIV,
|
|
masterKey,
|
|
encryptingAlgorithm,
|
|
signingAlgorithm,
|
|
keyDerivationIterationCount);
|
|
if (!ByteArraysEqual(computedSignature, signature))
|
|
{
|
|
throw new CryptographicException(@"Invalid Signature.");
|
|
}
|
|
signingAlgorithm.Clear();
|
|
signingAlgorithm.Dispose();
|
|
|
|
// The signature is valid, so now we can work on decrypting the data.
|
|
var ivLength = encryptingAlgorithm.BlockSize / 8;
|
|
byte[] initializationVector = new byte[ivLength];
|
|
byte[] cipherText = new byte[cipherTextAndIV.Length - ivLength];
|
|
// The IV is embedded in the cipher text, so we extract it out.
|
|
Buffer.BlockCopy(cipherTextAndIV, 0, initializationVector, 0, ivLength);
|
|
// Then we get the encrypted data.
|
|
Buffer.BlockCopy(cipherTextAndIV, ivLength, cipherText, 0, cipherTextAndIV.Length - ivLength);
|
|
|
|
encryptingAlgorithm.Key = DerivedEncryptionKey(
|
|
masterKey,
|
|
encryptingAlgorithm,
|
|
keyDerivationIterationCount);
|
|
encryptingAlgorithm.IV = initializationVector;
|
|
|
|
// Decrypt
|
|
using (var ms = new MemoryStream())
|
|
using (var cs = new CryptoStream(ms, encryptingAlgorithm.CreateDecryptor(), CryptoStreamMode.Write))
|
|
{
|
|
cs.Write(cipherText);
|
|
cs.FlushFinalBlock();
|
|
plainText = ms.ToArray();
|
|
}
|
|
encryptingAlgorithm.Clear();
|
|
encryptingAlgorithm.Dispose();
|
|
|
|
// And convert from the bytes back to a string.
|
|
return Encoding.UTF8.GetString(plainText);
|
|
}
|
|
|
|
public byte[] MasterKey(string keyId)
|
|
{
|
|
return Convert.FromBase64String(_keyRing[keyId]);
|
|
}
|
|
|
|
private byte[] SignData(byte[] cipherText, byte[] masterKey, SymmetricAlgorithm symmetricAlgorithm, KeyedHashAlgorithm hashAlgorithm, int keyDerivationIterationCount)
|
|
{
|
|
hashAlgorithm.Key = DerivedSigningKey(masterKey, symmetricAlgorithm, keyDerivationIterationCount);
|
|
byte[] signature = hashAlgorithm.ComputeHash(cipherText);
|
|
hashAlgorithm.Clear();
|
|
return signature;
|
|
}
|
|
|
|
private byte[] DerivedSigningKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
|
|
{
|
|
return KeyDerivation.Pbkdf2(
|
|
@"IdentityLookupDataSigning",
|
|
key,
|
|
KeyDerivationPrf.HMACSHA512,
|
|
keyDerivationIterationCount,
|
|
algorithm.KeySize / 8);
|
|
}
|
|
|
|
private byte[] DerivedEncryptionKey(byte[] key, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
|
|
{
|
|
return KeyDerivation.Pbkdf2(
|
|
@"IdentityLookupEncryption",
|
|
key,
|
|
KeyDerivationPrf.HMACSHA512,
|
|
keyDerivationIterationCount,
|
|
algorithm.KeySize / 8);
|
|
}
|
|
|
|
private byte[] DerivedInitializationVector(byte[] key, string plainText, SymmetricAlgorithm algorithm, int keyDerivationIterationCount)
|
|
{
|
|
return KeyDerivation.Pbkdf2(
|
|
plainText,
|
|
key,
|
|
KeyDerivationPrf.HMACSHA512,
|
|
keyDerivationIterationCount,
|
|
algorithm.BlockSize / 8);
|
|
}
|
|
|
|
private byte[] CombineByteArrays(byte[] left, byte[] right)
|
|
{
|
|
byte[] output = new byte[left.Length + right.Length];
|
|
Buffer.BlockCopy(left, 0, output, 0, left.Length);
|
|
Buffer.BlockCopy(right, 0, output, left.Length, right.Length);
|
|
|
|
return output;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoOptimization)]
|
|
private static bool ByteArraysEqual(byte[] a, byte[] b)
|
|
{
|
|
if (ReferenceEquals(a, b))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (a == null || b == null || a.Length != b.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool areSame = true;
|
|
for (int i = 0; i < a.Length; i++)
|
|
{
|
|
areSame &= (a[i] == b[i]);
|
|
}
|
|
return areSame;
|
|
}
|
|
|
|
} |