Improve the binary storage format of encrypted secrets. Move the related code from AesGcmHelper into the EncryptionService.
This commit is contained in:
parent
ac08956339
commit
4201d0240d
11 changed files with 110 additions and 131 deletions
|
|
@ -2,6 +2,6 @@ namespace IdentityShroud.Core.Contracts;
|
|||
|
||||
public interface IEncryptionService
|
||||
{
|
||||
byte[] Encrypt(byte[] plain);
|
||||
byte[] Encrypt(ReadOnlyMemory<byte> plain);
|
||||
byte[] Decrypt(ReadOnlyMemory<byte> cipher);
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public static class AesGcmHelper
|
||||
{
|
||||
|
||||
public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
|
||||
{
|
||||
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];
|
||||
|
||||
aes.Encrypt(nonce, plaintext, ciphertext, tag);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 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(ReadOnlyMemory<byte> payload, byte[] key)
|
||||
{
|
||||
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 = payload.Span[..nonceSize];
|
||||
ReadOnlySpan<byte> ciphertext = payload.Span.Slice(nonceSize, payload.Length - nonceSize - tagSize);
|
||||
ReadOnlySpan<byte> tag = payload.Span.Slice(payload.Length - tagSize, tagSize);
|
||||
|
||||
byte[] plaintext = new byte[ciphertext.Length];
|
||||
|
||||
using var aes = new AesGcm(key, tagSize);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public static class RsaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Load RSA private key from PKCS#8 format
|
||||
/// </summary>
|
||||
public static RSA LoadFromPkcs8(byte[] pkcs8Key)
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
|
||||
return rsa;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
|
||||
|
|
@ -8,20 +9,85 @@ namespace IdentityShroud.Core.Services;
|
|||
/// </summary>
|
||||
public class EncryptionService : IEncryptionService
|
||||
{
|
||||
private readonly byte[] encryptionKey;
|
||||
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"));
|
||||
_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.");
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] plain)
|
||||
public byte[] Encrypt(ReadOnlyMemory<byte> plaintext)
|
||||
{
|
||||
return AesGcmHelper.EncryptAesGcm(plain, encryptionKey);
|
||||
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> cipher)
|
||||
public byte[] Decrypt(ReadOnlyMemory<byte> input)
|
||||
{
|
||||
return AesGcmHelper.DecryptAesGcm(cipher, encryptionKey);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue