2026-02-22 19:11:17 +01:00
|
|
|
|
using System.Security.Cryptography;
|
2026-02-15 19:18:02 +01:00
|
|
|
|
using IdentityShroud.Core.Contracts;
|
|
|
|
|
|
using IdentityShroud.Core.Security;
|
|
|
|
|
|
|
|
|
|
|
|
namespace IdentityShroud.Core.Services;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class EncryptionService : IEncryptionService
|
|
|
|
|
|
{
|
2026-02-22 19:11:17 +01:00
|
|
|
|
private record struct AlgVersion(int NonceSize, int TagSize);
|
|
|
|
|
|
|
|
|
|
|
|
private AlgVersion[] _versions =
|
|
|
|
|
|
[
|
|
|
|
|
|
new(0, 0), // version 0 does not realy exist
|
|
|
|
|
|
new (12, 16), // version 1
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
private readonly byte[] _encryptionKey;
|
2026-02-15 19:18:02 +01:00
|
|
|
|
|
|
|
|
|
|
public EncryptionService(ISecretProvider secretProvider)
|
|
|
|
|
|
{
|
2026-02-22 19:11:17 +01:00
|
|
|
|
_encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master"));
|
|
|
|
|
|
if (_encryptionKey.Length != 32) // 256‑bit key
|
|
|
|
|
|
throw new Exception("Key must be 256 bits (32 bytes) for AES‑256‑GCM.");
|
2026-02-15 19:18:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 19:11:17 +01:00
|
|
|
|
public byte[] Encrypt(ReadOnlyMemory<byte> plaintext)
|
2026-02-15 19:18:02 +01:00
|
|
|
|
{
|
2026-02-22 19:11:17 +01:00
|
|
|
|
const int versionNumber = 1;
|
|
|
|
|
|
AlgVersion versionParams = _versions[versionNumber];
|
|
|
|
|
|
|
|
|
|
|
|
int resultSize = 1 + versionParams.NonceSize + versionParams.TagSize + plaintext.Length;
|
|
|
|
|
|
// allocate buffer for complete response
|
|
|
|
|
|
var result = new byte[resultSize];
|
|
|
|
|
|
|
|
|
|
|
|
result[0] = (byte)versionNumber;
|
|
|
|
|
|
|
|
|
|
|
|
// make the spans that point to the parts of the result where their data is located
|
|
|
|
|
|
var nonce = result.AsSpan(1, versionParams.NonceSize);
|
|
|
|
|
|
var tag = result.AsSpan(1 + versionParams.NonceSize, versionParams.TagSize);
|
|
|
|
|
|
var cipher = result.AsSpan(1 + versionParams.NonceSize + versionParams.TagSize);
|
|
|
|
|
|
|
|
|
|
|
|
// use the spans to place the data directly in its place
|
|
|
|
|
|
RandomNumberGenerator.Fill(nonce);
|
|
|
|
|
|
using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
|
|
|
|
|
|
aes.Encrypt(nonce, plaintext.Span, cipher, tag);
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-02-15 19:18:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 19:11:17 +01:00
|
|
|
|
public byte[] Decrypt(ReadOnlyMemory<byte> input)
|
2026-02-15 19:18:02 +01:00
|
|
|
|
{
|
2026-02-22 19:11:17 +01:00
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------
|
|
|
|
|
|
// 1️⃣ Extract the three components.
|
|
|
|
|
|
// ----------------------------------------------------------------
|
|
|
|
|
|
// AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
|
|
|
|
|
|
// AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag)
|
|
|
|
|
|
//int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
|
|
|
|
|
|
//int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
|
|
|
|
|
|
var payload = input.Span;
|
|
|
|
|
|
int versionNumber = (int)payload[0];
|
|
|
|
|
|
if (versionNumber != 1)
|
|
|
|
|
|
throw new ArgumentException("Invalid payloag");
|
|
|
|
|
|
|
|
|
|
|
|
AlgVersion versionParams = _versions[versionNumber];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.Length < 1 + versionParams.NonceSize + versionParams.TagSize)
|
|
|
|
|
|
throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
|
|
|
|
|
|
|
|
|
|
|
|
ReadOnlySpan<byte> nonce = payload.Slice(1, versionParams.NonceSize);
|
|
|
|
|
|
ReadOnlySpan<byte> tag = payload.Slice(1 + versionParams.NonceSize, versionParams.TagSize);
|
|
|
|
|
|
ReadOnlySpan<byte> cipher = payload.Slice(1 + versionParams.NonceSize + versionParams.TagSize);
|
|
|
|
|
|
|
|
|
|
|
|
byte[] plaintext = new byte[cipher.Length];
|
|
|
|
|
|
|
|
|
|
|
|
using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
aes.Decrypt(nonce, cipher, tag, plaintext);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (CryptographicException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Tag verification failed → tampering or wrong key/nonce.
|
|
|
|
|
|
throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return plaintext;
|
2026-02-15 19:18:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|