Support rotation of master key.
The EncryptionService now loads a set of keys and uses the active one to encrypt and selects key based on keyid during decryption. Introduced EncryptedValue to hold keyId and encrypted data. (There are no intermeddiate keys yet)
This commit is contained in:
parent
4201d0240d
commit
644b005f2a
19 changed files with 259 additions and 72 deletions
|
|
@ -2,6 +2,6 @@ namespace IdentityShroud.Core.Contracts;
|
|||
|
||||
public interface IEncryptionService
|
||||
{
|
||||
byte[] Encrypt(ReadOnlyMemory<byte> plain);
|
||||
byte[] Decrypt(ReadOnlyMemory<byte> cipher);
|
||||
EncryptedValue Encrypt(ReadOnlyMemory<byte> plain);
|
||||
byte[] Decrypt(EncryptedValue input);
|
||||
}
|
||||
|
|
@ -3,4 +3,10 @@ namespace IdentityShroud.Core.Contracts;
|
|||
public interface ISecretProvider
|
||||
{
|
||||
string GetSecret(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Should return one active key, might return inactive keys.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
EncryptionKey[] GetKeys(string name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
|
|
@ -11,5 +12,5 @@ public class ClientSecret
|
|||
public Guid ClientId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
public required byte[] SecretEncrypted { get; set; }
|
||||
public required EncryptedValue Secret { get; set; }
|
||||
}
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
|
||||
[Table("realm_key")]
|
||||
public record RealmKey(Guid Id, string KeyType, byte[] KeyDataEncrypted, DateTime CreatedAt)
|
||||
public record RealmKey
|
||||
{
|
||||
public Guid Id { get; private set; } = Id;
|
||||
public string KeyType { get; private set; } = KeyType;
|
||||
public byte[] KeyDataEncrypted { get; private set; } = KeyDataEncrypted;
|
||||
public DateTime CreatedAt { get; private set; } = CreatedAt;
|
||||
public required Guid Id { get; init; }
|
||||
public required string KeyType { get; init; }
|
||||
|
||||
|
||||
public required EncryptedValue Key { get; init; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -14,4 +14,9 @@ public class ConfigurationSecretProvider(IConfiguration configuration) : ISecret
|
|||
{
|
||||
return secrets.GetValue<string>(name) ?? "";
|
||||
}
|
||||
|
||||
public EncryptionKey[] GetKeys(string name)
|
||||
{
|
||||
return secrets.GetSection(name).Get<EncryptionKey[]>() ?? [];
|
||||
}
|
||||
}
|
||||
6
IdentityShroud.Core/Security/EncryptedValue.cs
Normal file
6
IdentityShroud.Core/Security/EncryptedValue.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
[Owned]
|
||||
public record EncryptedValue(string KeyId, byte[] Value);
|
||||
4
IdentityShroud.Core/Security/EncryptionKey.cs
Normal file
4
IdentityShroud.Core/Security/EncryptionKey.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
// Contains an encryption key and associated relevant data
|
||||
public record EncryptionKey(string Id, bool Active, string Algorithm, byte[] Key);
|
||||
|
|
@ -57,7 +57,7 @@ public class ClientService(
|
|||
return new ClientSecret()
|
||||
{
|
||||
CreatedAt = clock.UtcNow(),
|
||||
SecretEncrypted = cryptor.Encrypt(secret),
|
||||
Secret = cryptor.Encrypt(secret),
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
|
||||
namespace IdentityShroud.Core.Services;
|
||||
|
||||
|
|
@ -17,16 +16,21 @@ public class EncryptionService : IEncryptionService
|
|||
new (12, 16), // version 1
|
||||
];
|
||||
|
||||
private readonly byte[] _encryptionKey;
|
||||
// Note this array is expected to have one item in it most of the during key rotation it will have two
|
||||
// until it is ensured the old key can safely be removed. More then two will work but is not really expected.
|
||||
private readonly EncryptionKey[] _encryptionKeys;
|
||||
|
||||
private EncryptionKey ActiveKey => _encryptionKeys.Single(k => k.Active);
|
||||
private EncryptionKey GetKey(string keyId) => _encryptionKeys.Single(k => k.Id == keyId);
|
||||
|
||||
public EncryptionService(ISecretProvider secretProvider)
|
||||
{
|
||||
_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.");
|
||||
_encryptionKeys = secretProvider.GetKeys("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(ReadOnlyMemory<byte> plaintext)
|
||||
public EncryptedValue Encrypt(ReadOnlyMemory<byte> plaintext)
|
||||
{
|
||||
const int versionNumber = 1;
|
||||
AlgVersion versionParams = _versions[versionNumber];
|
||||
|
|
@ -44,26 +48,21 @@ public class EncryptionService : IEncryptionService
|
|||
|
||||
// use the spans to place the data directly in its place
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
|
||||
var encryptionKey = ActiveKey;
|
||||
using var aes = new AesGcm(encryptionKey.Key, versionParams.TagSize);
|
||||
aes.Encrypt(nonce, plaintext.Span, cipher, tag);
|
||||
|
||||
return result;
|
||||
return new (encryptionKey.Id, result);
|
||||
}
|
||||
|
||||
public byte[] Decrypt(ReadOnlyMemory<byte> input)
|
||||
public byte[] Decrypt(EncryptedValue input)
|
||||
{
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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;
|
||||
var encryptionKey = GetKey(input.KeyId);
|
||||
|
||||
var payload = input.Value.AsSpan();
|
||||
int versionNumber = (int)payload[0];
|
||||
if (versionNumber != 1)
|
||||
throw new ArgumentException("Invalid payloag");
|
||||
throw new ArgumentException("Invalid payload");
|
||||
|
||||
AlgVersion versionParams = _versions[versionNumber];
|
||||
|
||||
|
|
@ -77,7 +76,7 @@ public class EncryptionService : IEncryptionService
|
|||
|
||||
byte[] plaintext = new byte[cipher.Length];
|
||||
|
||||
using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
|
||||
using var aes = new AesGcm(encryptionKey.Key, versionParams.TagSize);
|
||||
try
|
||||
{
|
||||
aes.Decrypt(nonce, cipher, tag, plaintext);
|
||||
|
|
|
|||
|
|
@ -29,23 +29,18 @@ public class KeyService(
|
|||
|
||||
IKeyProvider provider = keyProviderFactory.CreateProvider(realmKey.KeyType);
|
||||
provider.SetJwkParameters(
|
||||
cryptor.Decrypt(realmKey.KeyDataEncrypted),
|
||||
cryptor.Decrypt(realmKey.Key),
|
||||
jwk);
|
||||
|
||||
return jwk;
|
||||
}
|
||||
|
||||
private RealmKey CreateKey(string keyType, byte[] plainKey) =>
|
||||
new RealmKey(
|
||||
Guid.NewGuid(),
|
||||
keyType,
|
||||
cryptor.Encrypt(plainKey),
|
||||
clock.UtcNow());
|
||||
|
||||
// public byte[] GetPrivateKey(IEncryptionService encryptionService)
|
||||
// {
|
||||
// if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
|
||||
// _privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
|
||||
// return _privateKeyDecrypted;
|
||||
// }
|
||||
new RealmKey()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
KeyType = keyType,
|
||||
Key = cryptor.Encrypt(plainKey),
|
||||
CreatedAt = clock.UtcNow(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue