2026-02-06 19:58:01 +01:00
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
|
|
|
|
|
|
namespace IdentityShroud.Core.Security;
|
|
|
|
|
|
|
|
|
|
|
|
public static class AesGcmHelper
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
|
|
|
|
|
|
{
|
2026-02-15 19:06:09 +01:00
|
|
|
|
int tagSize = AesGcm.TagByteSizes.MaxSize;
|
|
|
|
|
|
using var aes = new AesGcm(key, tagSize);
|
|
|
|
|
|
|
|
|
|
|
|
Span<byte> nonce = stackalloc byte[AesGcm.NonceByteSizes.MaxSize];
|
|
|
|
|
|
RandomNumberGenerator.Fill(nonce);
|
|
|
|
|
|
Span<byte> ciphertext = stackalloc byte[plaintext.Length];
|
|
|
|
|
|
Span<byte> tag = stackalloc byte[tagSize];
|
2026-02-06 19:58:01 +01:00
|
|
|
|
|
|
|
|
|
|
aes.Encrypt(nonce, plaintext, ciphertext, tag);
|
2026-02-15 19:06:09 +01:00
|
|
|
|
|
|
|
|
|
|
// Return concatenated nonce|ciphertext|tag
|
|
|
|
|
|
var result = new byte[nonce.Length + ciphertext.Length + tag.Length];
|
|
|
|
|
|
nonce.CopyTo(result.AsSpan(0, nonce.Length));
|
|
|
|
|
|
ciphertext.CopyTo(result.AsSpan(nonce.Length, ciphertext.Length));
|
|
|
|
|
|
tag.CopyTo(result.AsSpan(nonce.Length + ciphertext.Length, tag.Length));
|
|
|
|
|
|
return result;
|
2026-02-06 19:58:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
|
|
// DecryptAesGcm
|
|
|
|
|
|
// • key – 32‑byte (256‑bit) secret key (same key used for encryption)
|
|
|
|
|
|
// • payload – byte[] containing nonce‖ciphertext‖tag
|
|
|
|
|
|
// • returns – the original plaintext bytes
|
|
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
|
|
public static byte[] DecryptAesGcm(byte[] payload, byte[] key)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (payload == null) throw new ArgumentNullException(nameof(payload));
|
|
|
|
|
|
if (key == null) throw new ArgumentNullException(nameof(key));
|
|
|
|
|
|
if (key.Length != 32) // 256‑bit key
|
|
|
|
|
|
throw new ArgumentException("Key must be 256 bits (32 bytes) for AES‑256‑GCM.", nameof(key));
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.Length < nonceSize + tagSize)
|
|
|
|
|
|
throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
|
|
|
|
|
|
|
|
|
|
|
|
ReadOnlySpan<byte> nonce = new(payload, 0, nonceSize);
|
|
|
|
|
|
ReadOnlySpan<byte> ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
|
|
|
|
|
|
ReadOnlySpan<byte> tag = new(payload, payload.Length - tagSize, tagSize);
|
|
|
|
|
|
|
|
|
|
|
|
byte[] plaintext = new byte[ciphertext.Length];
|
|
|
|
|
|
|
2026-02-15 19:06:09 +01:00
|
|
|
|
using var aes = new AesGcm(key, tagSize);
|
2026-02-06 19:58:01 +01:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
aes.Decrypt(nonce, ciphertext, tag, plaintext);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (CryptographicException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Tag verification failed → tampering or wrong key/nonce.
|
|
|
|
|
|
throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return plaintext;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|