Files
baya-monorepo/server/src/Infrastructure/CleanArc.Infrastructure.Identity/Identity/DataProtection/LookupProtector.cs
T
2026-06-16 01:32:43 +03:30

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;
}
}