Encrypt realm data with dek which is encrypted with kek. The signing keys are also encrypted with the kek.

This commit is contained in:
eelke 2026-02-26 16:53:02 +01:00
parent 644b005f2a
commit 650fe99990
36 changed files with 399 additions and 129 deletions

View file

@ -1,5 +1,6 @@
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Substitutes;
@ -10,12 +11,17 @@ namespace IdentityShroud.Core.Tests.Services;
public class ClientServiceTests : IClassFixture<DbFixture>
{
private readonly DbFixture _dbFixture;
private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough();
//private readonly IDekEncryptionService _dekEncryptionService = EncryptionServiceSubstitute.CreatePassthrough();
private readonly IDataEncryptionService _dataEncryptionService = Substitute.For<IDataEncryptionService>();
private readonly IClock _clock = Substitute.For<IClock>();
private readonly Guid _realmId = new("a1b2c3d4-0000-0000-0000-000000000001");
public ClientServiceTests(DbFixture dbFixture)
{
_dataEncryptionService.Encrypt(Arg.Any<ReadOnlyMemory<byte>>())
.Returns(x => new EncryptedValue(DekId.NewId(), x.ArgAt<ReadOnlyMemory<byte>>(0).ToArray()));
_dbFixture = dbFixture;
using Db db = dbFixture.CreateDbContext();
if (!db.Database.EnsureCreated())
@ -51,7 +57,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using (var db = _dbFixture.CreateDbContext())
{
// Act
ClientService sut = new(db, _encryptionService, _clock);
ClientService sut = new(db, _dataEncryptionService, _clock);
var response = await sut.Create(
_realmId,
new ClientCreateRequest
@ -107,7 +113,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
ClientService sut = new(actContext, _encryptionService, _clock);
ClientService sut = new(actContext, _dataEncryptionService, _clock);
Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken);
// Verify
@ -142,7 +148,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
ClientService sut = new(actContext, _encryptionService, _clock);
ClientService sut = new(actContext, _dataEncryptionService, _clock);
Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken);
// Verify

View file

@ -1,9 +1,10 @@
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Tests.Services;
public class EncryptionServiceTests
public class DekEncryptionServiceTests
{
[Fact]
public void RoundtripWorks()
@ -13,9 +14,9 @@ public class EncryptionServiceTests
// setup
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
var secretProvider = Substitute.For<ISecretProvider>();
EncryptionKey[] keys =
KeyEncryptionKey[] keys =
[
new EncryptionKey("1", true, "AES", keyValue)
new KeyEncryptionKey(KekId.NewId(), true, "AES", keyValue)
];
secretProvider.GetKeys("master").Returns(keys);
@ -23,68 +24,38 @@ public class EncryptionServiceTests
ReadOnlySpan<byte> input = "Hello, World!"u8;
// act
EncryptionService sut = new(secretProvider);
EncryptedValue cipher = sut.Encrypt(input.ToArray());
DekEncryptionService sut = new(secretProvider);
EncryptedDek cipher = sut.Encrypt(input.ToArray());
byte[] result = sut.Decrypt(cipher);
// verify
Assert.Equal(input, result);
}
[Fact]
public void DecodeV1_Success()
{
// When introducing a new version we need version specific tests to
// make sure decoding of legacy data still works.
// setup
byte[] cipher =
[
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
];
EncryptedValue secret = new("kid", cipher);
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
var secretProvider = Substitute.For<ISecretProvider>();
EncryptionKey[] keys =
[
new EncryptionKey("kid", true, "AES", keyValue)
];
secretProvider.GetKeys("master").Returns(keys);
// act
EncryptionService sut = new(secretProvider);
byte[] result = sut.Decrypt(secret);
// verify
Assert.Equal("Hello, World!"u8, result);
}
[Fact]
public void DetectsCorruptInput()
{
// When introducing a new version we need version specific tests to
// make sure decoding of legacy data still works.
KekId kid = KekId.NewId();
// setup
byte[] cipher = // NOTE INCORRECT CIPHER DO NOT USE IN OTHER TESTS
[
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
193, 75, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
];
EncryptedValue secret = new("kid", cipher);
EncryptedDek secret = new(kid, cipher);
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
var secretProvider = Substitute.For<ISecretProvider>();
EncryptionKey[] keys =
KeyEncryptionKey[] keys =
[
new EncryptionKey("kid", true, "AES", keyValue)
new KeyEncryptionKey(kid, true, "AES", keyValue)
];
secretProvider.GetKeys("master").Returns(keys);
// act
EncryptionService sut = new(secretProvider);
DekEncryptionService sut = new(secretProvider);
Assert.Throws<InvalidOperationException>(
() => sut.Decrypt(secret),
ex => ex.Message.Contains("Decryption failed") ? null : "Expected Decryption failed in message");
@ -96,25 +67,28 @@ public class EncryptionServiceTests
// The key is marked inactive also it is the second key
// setup
KekId kid1 = KekId.NewId();
KekId kid2 = KekId.NewId();
byte[] cipher =
[
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
];
EncryptedValue secret = new("1", cipher);
EncryptedDek secret = new(kid1, cipher);
byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
var secretProvider = Substitute.For<ISecretProvider>();
EncryptionKey[] keys =
KeyEncryptionKey[] keys =
[
new EncryptionKey("2", true, "AES", keyValue2),
new EncryptionKey("1", false, "AES", keyValue1),
new KeyEncryptionKey(kid2, true, "AES", keyValue2),
new KeyEncryptionKey(kid1, false, "AES", keyValue1),
];
secretProvider.GetKeys("master").Returns(keys);
// act
EncryptionService sut = new(secretProvider);
DekEncryptionService sut = new(secretProvider);
byte[] result = sut.Decrypt(secret);
// verify
@ -125,22 +99,25 @@ public class EncryptionServiceTests
public void EncryptionUsesActiveKey()
{
// setup
KekId kid1 = KekId.NewId();
KekId kid2 = KekId.NewId();
byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
var secretProvider = Substitute.For<ISecretProvider>();
EncryptionKey[] keys =
KeyEncryptionKey[] keys =
[
new EncryptionKey("1", false, "AES", keyValue1),
new EncryptionKey("2", true, "AES", keyValue2),
new KeyEncryptionKey(kid1, false, "AES", keyValue1),
new KeyEncryptionKey(kid2, true, "AES", keyValue2),
];
secretProvider.GetKeys("master").Returns(keys);
ReadOnlySpan<byte> input = "Hello, World!"u8;
// act
EncryptionService sut = new(secretProvider);
EncryptedValue cipher = sut.Encrypt(input.ToArray());
DekEncryptionService sut = new(secretProvider);
EncryptedDek cipher = sut.Encrypt(input.ToArray());
// Verify
Assert.Equal("2", cipher.KeyId);
Assert.Equal(kid2, cipher.KekId);
}
}

View file

@ -0,0 +1,30 @@
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Tests.Services;
public class EncryptionTests
{
[Fact]
public void DecodeV1_Success()
{
// When introducing a new version we need version specific tests to
// make sure decoding of legacy data still works.
// setup
byte[] cipher =
[
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
];
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
// act
byte[] result = Encryption.Decrypt(cipher, keyValue);
// verify
Assert.Equal("Hello, World!"u8, result);
}
}

View file

@ -1,5 +1,6 @@
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Security.Keys;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
@ -43,7 +44,7 @@ public class RealmServiceTests : IClassFixture<DbFixture>
{
Id = Guid.NewGuid(),
KeyType = "TST",
Key = new("kid", [21]),
Key = new(KekId.NewId(), [21]),
CreatedAt = DateTime.UtcNow
});
// Act