diff --git a/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs b/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs new file mode 100644 index 0000000..923a865 --- /dev/null +++ b/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs @@ -0,0 +1,36 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using IdentityShroud.Core.Helpers; + +namespace IdentityShroud.Core.Tests.Helpers; + +public class Base64UrlConverterTests +{ + internal class Data + { + [JsonConverter(typeof(Base64UrlConverter))] + public byte[]? X { get; set; } + } + + [Fact] + public void Serialize() + { + Data d = new() { X = ">>>???"u8.ToArray() }; + string s = JsonSerializer.Serialize(d); + + Assert.Contains("\"Pj4-Pz8_\"", s); + } + + [Fact] + public void Deerialize() + { + var jsonstring = """ + { "X": "Pj4-Pz8_" } + """; + var d = JsonSerializer.Deserialize(jsonstring); + + Assert.Equal(">>>???", Encoding.UTF8.GetString(d.X)); + } + +} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs new file mode 100644 index 0000000..4f88e48 --- /dev/null +++ b/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs @@ -0,0 +1,64 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Model; +using IdentityShroud.Core.Security; +using IdentityShroud.Core.Services; +using IdentityShroud.TestUtils.Substitutes; + +namespace IdentityShroud.Core.Tests.Services; + +public class DataEncryptionServiceTests +{ + private readonly IRealmContext _realmContext = Substitute.For(); + private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();// Substitute.For(); + + private readonly DekId _activeDekId = DekId.NewId(); + private readonly DekId _secondDekId = DekId.NewId(); + private DataEncryptionService CreateSut() + => new(_realmContext, _dekCryptor); + + [Fact] + public void Encrypt_UsesActiveKey() + { + _realmContext.GetDeks(Arg.Any()).Returns([ + CreateRealmDek(_secondDekId, false), + CreateRealmDek(_activeDekId, true), + ]); + + var cipher = CreateSut().Encrypt("Hello"u8); + + Assert.Equal(_activeDekId, cipher.DekId); + } + + [Fact] + public void Decrypt_UsesCorrectKey() + { + var first = CreateRealmDek(_activeDekId, true); + _realmContext.GetDeks(Arg.Any()).Returns([ first ]); + + var sut = CreateSut(); + var cipher = sut.Encrypt("Hello"u8); + + // Deactivate original key + first.Active = false; + // Make new active + var second = CreateRealmDek(_secondDekId, true); + // Return both + _realmContext.GetDeks(Arg.Any()).Returns([ first, second ]); + + + var decoded = sut.Decrypt(cipher); + + Assert.Equal("Hello"u8, decoded); + } + + private RealmDek CreateRealmDek(DekId id, bool active) + => new() + { + Id = id, + Active = active, + Algorithm = "AES", + KeyData = new(KekId.NewId(), RandomNumberGenerator.GetBytes(32)), + RealmId = default, + }; +} \ No newline at end of file diff --git a/IdentityShroud.Core/DTO/JsonWebKey.cs b/IdentityShroud.Core/DTO/JsonWebKey.cs index ea4d7d5..4f16955 100644 --- a/IdentityShroud.Core/DTO/JsonWebKey.cs +++ b/IdentityShroud.Core/DTO/JsonWebKey.cs @@ -1,7 +1,5 @@ -using System.Buffers; -using System.Buffers.Text; -using System.Text.Json; using System.Text.Json.Serialization; +using IdentityShroud.Core.Helpers; namespace IdentityShroud.Core.Messages; @@ -48,26 +46,4 @@ public class JsonWebKey // [JsonPropertyName("x5t")] // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // public string? X509CertificateThumbprint { get; set; } -} - -public class Base64UrlConverter : JsonConverter -{ - public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - // GetValueSpan gives you the raw UTF-8 bytes of the JSON string value - if (reader.HasValueSequence) - { - var valueSequence = reader.ValueSequence.ToArray(); - return Base64Url.DecodeFromUtf8(valueSequence); - } - return Base64Url.DecodeFromUtf8(reader.ValueSpan); - } - - public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) - { - int encodedLength = Base64Url.GetEncodedLength(value.Length); - Span buffer = encodedLength <= 256 ? stackalloc byte[encodedLength] : new byte[encodedLength]; - Base64Url.EncodeToUtf8(value, buffer); - writer.WriteStringValue(buffer); - } } \ No newline at end of file diff --git a/IdentityShroud.Core/Helpers/Base64UrlConverter.cs b/IdentityShroud.Core/Helpers/Base64UrlConverter.cs new file mode 100644 index 0000000..77f05f2 --- /dev/null +++ b/IdentityShroud.Core/Helpers/Base64UrlConverter.cs @@ -0,0 +1,28 @@ +using System.Buffers; +using System.Buffers.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace IdentityShroud.Core.Helpers; + +public class Base64UrlConverter : JsonConverter +{ + public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // GetValueSpan gives you the raw UTF-8 bytes of the JSON string value + if (reader.HasValueSequence) + { + var valueSequence = reader.ValueSequence.ToArray(); + return Base64Url.DecodeFromUtf8(valueSequence); + } + return Base64Url.DecodeFromUtf8(reader.ValueSpan); + } + + public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) + { + int encodedLength = Base64Url.GetEncodedLength(value.Length); + Span buffer = encodedLength <= 256 ? stackalloc byte[encodedLength] : new byte[encodedLength]; + Base64Url.EncodeToUtf8(value, buffer); + writer.WriteStringValue(buffer); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Model/Realm.cs b/IdentityShroud.Core/Model/Realm.cs index f3e087a..bbe9631 100644 --- a/IdentityShroud.Core/Model/Realm.cs +++ b/IdentityShroud.Core/Model/Realm.cs @@ -33,7 +33,7 @@ public class Realm public record RealmDek { public required DekId Id { get; init; } - public required bool Active { get; init; } + public required bool Active { get; set; } public required string Algorithm { get; init; } public required EncryptedDek KeyData { get; init; } public required Guid RealmId { get; init; }