IdentityShroud/IdentityShroud.Core/Services/EncryptionService.cs

93 lines
No EOL
3.5 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Security;
namespace IdentityShroud.Core.Services;
/// <summary>
///
/// </summary>
public class EncryptionService : IEncryptionService
{
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;
public EncryptionService(ISecretProvider secretProvider)
{
_encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master"));
if (_encryptionKey.Length != 32) // 256bit key
throw new Exception("Key must be 256bits (32 bytes) for AES256GCM.");
}
public byte[] Encrypt(ReadOnlyMemory<byte> plaintext)
{
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;
}
public byte[] Decrypt(ReadOnlyMemory<byte> input)
{
// ----------------------------------------------------------------
// 1⃣ Extract the three components.
// ----------------------------------------------------------------
// AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
// AesGcm.TagByteSizes.MaxSize = 16 bytes (128bit 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;
}
}