diff --git a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs b/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs deleted file mode 100644 index db984f1..0000000 --- a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using IdentityShroud.Core; -using IdentityShroud.Core.Model; -using IdentityShroud.Core.Tests.Fixtures; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace IdentityShroud.Api.Tests.Apis; - -public class ClientApiTests : IClassFixture -{ - private readonly ApplicationFactory _factory; - - public ClientApiTests(ApplicationFactory factory) - { - _factory = factory; - - using var scope = _factory.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - if (!db.Database.EnsureCreated()) - { - db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;"); - } - } - - [Theory] - [InlineData(null, false, "ClientId")] - [InlineData("", false, "ClientId")] - [InlineData("my-client", true, "")] - public async Task Create_Validation(string? clientId, bool succeeds, string fieldName) - { - // setup - Realm realm = await CreateRealmAsync("test-realm", "Test Realm"); - - var client = _factory.CreateClient(); - - // act - var response = await client.PostAsync( - $"/api/v1/realms/{realm.Id}/clients", - JsonContent.Create(new { ClientId = clientId }), - TestContext.Current.CancellationToken); - -#if DEBUG - string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); -#endif - - if (succeeds) - { - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } - else - { - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var problemDetails = - await response.Content.ReadFromJsonAsync( - TestContext.Current.CancellationToken); - - Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName); - } - } - - [Fact] - public async Task Create_Success_ReturnsCreatedWithLocation() - { - // setup - Realm realm = await CreateRealmAsync("create-realm", "Create Realm"); - - var client = _factory.CreateClient(); - - // act - var response = await client.PostAsync( - $"/api/v1/realms/{realm.Id}/clients", - JsonContent.Create(new { ClientId = "new-client", Name = "New Client" }), - TestContext.Current.CancellationToken); - -#if DEBUG - string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); -#endif - - // verify - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - var body = await response.Content.ReadFromJsonAsync( - TestContext.Current.CancellationToken); - - Assert.NotNull(body); - Assert.Equal("new-client", body.ClientId); - Assert.True(body.Id > 0); - } - - [Fact] - public async Task Create_UnknownRealm_ReturnsNotFound() - { - var client = _factory.CreateClient(); - - var response = await client.PostAsync( - $"/api/v1/realms/{Guid.NewGuid()}/clients", - JsonContent.Create(new { ClientId = "some-client" }), - TestContext.Current.CancellationToken); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task Get_Success() - { - // setup - Realm realm = await CreateRealmAsync("get-realm", "Get Realm"); - Client dbClient = await CreateClientAsync(realm, "get-client", "Get Client"); - - var httpClient = _factory.CreateClient(); - - // act - var response = await httpClient.GetAsync( - $"/api/v1/realms/{realm.Id}/clients/{dbClient.Id}", - TestContext.Current.CancellationToken); - -#if DEBUG - string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); -#endif - - // verify - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadFromJsonAsync( - TestContext.Current.CancellationToken); - - Assert.NotNull(body); - Assert.Equal(dbClient.Id, body.Id); - Assert.Equal("get-client", body.ClientId); - Assert.Equal("Get Client", body.Name); - Assert.Equal(realm.Id, body.RealmId); - } - - [Fact] - public async Task Get_UnknownClient_ReturnsNotFound() - { - // setup - Realm realm = await CreateRealmAsync("notfound-realm", "NotFound Realm"); - - var httpClient = _factory.CreateClient(); - - // act - var response = await httpClient.GetAsync( - $"/api/v1/realms/{realm.Id}/clients/99999", - TestContext.Current.CancellationToken); - - // verify - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - private async Task CreateRealmAsync(string slug, string name) - { - using var scope = _factory.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var realm = new Realm { Slug = slug, Name = name }; - db.Realms.Add(realm); - await db.SaveChangesAsync(TestContext.Current.CancellationToken); - return realm; - } - - private async Task CreateClientAsync(Realm realm, string clientId, string? name = null) - { - using var scope = _factory.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var client = new Client - { - RealmId = realm.Id, - ClientId = clientId, - Name = name, - CreatedAt = DateTime.UtcNow, - }; - db.Clients.Add(client); - await db.SaveChangesAsync(TestContext.Current.CancellationToken); - return client; - } -} diff --git a/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs b/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs index ecc46c0..8d08a27 100644 --- a/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs +++ b/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs @@ -114,7 +114,7 @@ public class RealmApisTests : IClassFixture { // act var client = _factory.CreateClient(); - var response = await client.GetAsync($"/realms/{slug}/.well-known/openid-configuration", + var response = await client.GetAsync("/realms/bar/.well-known/openid-configuration", TestContext.Current.CancellationToken); // verify @@ -125,25 +125,23 @@ public class RealmApisTests : IClassFixture public async Task GetJwks() { // setup - IDekEncryptionService dekEncryptionService = _factory.Services.GetRequiredService(); + IEncryptionService encryptionService = _factory.Services.GetRequiredService(); using var rsa = RSA.Create(2048); RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false); - RealmKey realmKey = new() - { - Id = Guid.NewGuid(), - KeyType = "RSA", - Key = dekEncryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()), - CreatedAt = DateTime.UtcNow, - }; + RealmKey realmKey = new( + Guid.NewGuid(), + "RSA", + encryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()), + DateTime.UtcNow); await ScopedContextAsync(async db => { db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ realmKey ]}); await db.SaveChangesAsync(TestContext.Current.CancellationToken); }); - + // act var client = _factory.CreateClient(); var response = await client.GetAsync("/auth/realms/foo/openid-connect/jwks", diff --git a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs index 9846559..6f4c461 100644 --- a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs +++ b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs @@ -1,6 +1,11 @@ +using IdentityShroud.Core.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.TestHost; +using Npgsql; using Testcontainers.PostgreSql; namespace IdentityShroud.Core.Tests.Fixtures; @@ -28,10 +33,7 @@ public class ApplicationFactory : WebApplicationFactory, IAsyncLifetime new Dictionary { ["Db:ConnectionString"] = _postgresqlServer.GetConnectionString(), - ["secrets:master:0:Id"] = "94970f27-3d88-4223-9940-7dd57548f5b5", - ["secrets:master:0:Active"] = "true", - ["secrets:master:0:Algorithm"] = "AES", - ["secrets:master:0:Key"] = "GVd07qW0frRX9quPX/X62L88BeRR7+IzgRJHtG7ZzHw=", + ["Encryption:Master"] = "GVd07qW0frRX9quPX/X62L88BeRR7+IzgRJHtG7ZzHw=", }); }); diff --git a/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs b/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs new file mode 100644 index 0000000..767337e --- /dev/null +++ b/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs @@ -0,0 +1,17 @@ +using IdentityShroud.Api.Mappers; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Messages; +using IdentityShroud.TestUtils.Substitutes; +using Microsoft.AspNetCore.WebUtilities; + +namespace IdentityShroud.Api.Tests.Mappers; + +// public class KeyMapperTests +// { +// private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); +// +// [Fact] +// public void Test() +// { +// } +// } \ No newline at end of file diff --git a/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs b/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs index f423f54..196b15d 100644 --- a/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs +++ b/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs @@ -2,7 +2,6 @@ using System.Buffers.Text; using System.Security.Cryptography; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Model; -using IdentityShroud.Core.Security; using IdentityShroud.Core.Security.Keys; using IdentityShroud.Core.Services; using IdentityShroud.TestUtils.Substitutes; @@ -11,7 +10,8 @@ namespace IdentityShroud.Api.Tests.Mappers; public class KeyServiceTests { - private readonly NullDekEncryptionService _dekEncryptionService = new(); + private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); + //private readonly IKeyProviderFactory _keyProviderFactory = Substitute.For(); [Fact] public void Test() @@ -20,27 +20,24 @@ public class KeyServiceTests using RSA rsa = RSA.Create(2048); RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false); - - DekId kid = DekId.NewId(); - RealmKey realmKey = new() + RealmKey realmKey = new( + new("60bb79cf-4bac-4521-87f2-ac87cc15541f"), + "RSA", + rsa.ExportPkcs8PrivateKey(), + DateTime.UtcNow) { - Id = new("60bb79cf-4bac-4521-87f2-ac87cc15541f"), - KeyType = "RSA", - Key = new(_dekEncryptionService.KeyId, rsa.ExportPkcs8PrivateKey()), - CreatedAt = DateTime.UtcNow, Priority = 10, }; // Act - KeyService sut = new(_dekEncryptionService, new KeyProviderFactory(), new ClockService()); + KeyService sut = new(_encryptionService, new KeyProviderFactory(), new ClockService()); var jwk = sut.CreateJsonWebKey(realmKey); - Assert.NotNull(jwk); Assert.Equal("RSA", jwk.KeyType); Assert.Equal(realmKey.Id.ToString(), jwk.KeyId); Assert.Equal("sig", jwk.Use); Assert.Equal(parameters.Exponent, Base64Url.DecodeFromChars(jwk.Exponent)); Assert.Equal(parameters.Modulus, Base64Url.DecodeFromChars(jwk.Modulus)); } -} +} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/ClientApi.cs b/IdentityShroud.Api/Apis/ClientApi.cs index e595e34..fd3e804 100644 --- a/IdentityShroud.Api/Apis/ClientApi.cs +++ b/IdentityShroud.Api/Apis/ClientApi.cs @@ -1,14 +1,14 @@ using FluentResults; -using IdentityShroud.Api.Mappers; using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Messages.Realm; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; namespace IdentityShroud.Api; - public record ClientCreateReponse(int Id, string ClientId); /// @@ -34,18 +34,13 @@ public static class ClientApi .WithName(ClientGetRouteName); } - private static Ok ClientGet( - Guid realmId, - int clientId, - HttpContext context) + private static Task ClientGet(HttpContext context) { - Client client = (Client)context.Items["ClientEntity"]!; - return TypedResults.Ok(new ClientMapper().ToDto(client)); + throw new NotImplementedException(); } private static async Task, InternalServerError>> ClientCreate( - Guid realmId, ClientCreateRequest request, [FromServices] IClientService service, HttpContext context, @@ -69,5 +64,6 @@ public static class ClientApi ["realmId"] = realm.Id, ["clientId"] = client.Id, }); + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs b/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs deleted file mode 100644 index 80b5f13..0000000 --- a/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace IdentityShroud.Api; - -public record ClientRepresentation -{ - public int Id { get; set; } - public Guid RealmId { get; set; } - public required string ClientId { get; set; } - public string? Name { get; set; } - public string? Description { get; set; } - - public string? SignatureAlgorithm { get; set; } - - public bool AllowClientCredentialsFlow { get; set; } = false; - - public required DateTime CreatedAt { get; set; } -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs b/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs index 771be81..8030153 100644 --- a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs +++ b/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs @@ -7,9 +7,8 @@ public class ClientIdValidationFilter(IClientService clientService) : IEndpointF { public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { - Guid realmId = context.Arguments.OfType().First(); int id = context.Arguments.OfType().First(); - Client? client = await clientService.FindById(realmId, id, context.HttpContext.RequestAborted); + Client? client = await clientService.FindById(id, context.HttpContext.RequestAborted); if (client is null) { return Results.NotFound(); diff --git a/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs b/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs index 75338e1..862b599 100644 --- a/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs +++ b/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs @@ -1,5 +1,6 @@ using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; namespace IdentityShroud.Api; diff --git a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs b/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs deleted file mode 100644 index 8e58717..0000000 --- a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs +++ /dev/null @@ -1,11 +0,0 @@ -using IdentityShroud.Core.Model; -using Riok.Mapperly.Abstractions; - -namespace IdentityShroud.Api.Mappers; - -[Mapper] -public partial class ClientMapper -{ - [MapperIgnoreSource(nameof(Client.Secrets))] - public partial ClientRepresentation ToDto(Client client); -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs b/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs index 7155208..36bd200 100644 --- a/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs +++ b/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs @@ -1,6 +1,9 @@ +using System.Security.Cryptography; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Security; +using Microsoft.AspNetCore.WebUtilities; namespace IdentityShroud.Api.Mappers; diff --git a/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs b/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs deleted file mode 100644 index 7666b36..0000000 --- a/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentValidation; -using IdentityShroud.Core.Contracts; - -namespace IdentityShroud.Api; - -public class ClientCreateRequestValidator : AbstractValidator -{ - // most of standard ascii minus the control characters and space - private const string ClientIdPattern = "^[\x21-\x7E]+"; - - private string[] AllowedAlgorithms = [ "RS256", "ES256" ]; - - public ClientCreateRequestValidator() - { - RuleFor(e => e.ClientId).NotEmpty().MaximumLength(40).Matches(ClientIdPattern); - RuleFor(e => e.Name).MaximumLength(80); - RuleFor(e => e.Description).MaximumLength(2048); - RuleFor(e => e.SignatureAlgorithm) - .Must(v => v is null || AllowedAlgorithms.Contains(v)) - .WithMessage($"SignatureAlgorithm must be one of {string.Join(", ", AllowedAlgorithms)} or null"); - } -} \ No newline at end of file diff --git a/IdentityShroud.Api/AppJsonSerializerContext.cs b/IdentityShroud.Api/AppJsonSerializerContext.cs index e7d90da..9b075ce 100644 --- a/IdentityShroud.Api/AppJsonSerializerContext.cs +++ b/IdentityShroud.Api/AppJsonSerializerContext.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using IdentityShroud.Core.Messages; using IdentityShroud.Core.Messages.Realm; +using Microsoft.Extensions.Diagnostics.HealthChecks; [JsonSerializable(typeof(OpenIdConfiguration))] [JsonSerializable(typeof(RealmCreateRequest))] diff --git a/IdentityShroud.Api/IdentityShroud.Api.csproj b/IdentityShroud.Api/IdentityShroud.Api.csproj index 31f88b2..72b4639 100644 --- a/IdentityShroud.Api/IdentityShroud.Api.csproj +++ b/IdentityShroud.Api/IdentityShroud.Api.csproj @@ -17,7 +17,7 @@ - + diff --git a/IdentityShroud.Api/Program.cs b/IdentityShroud.Api/Program.cs index 29f6736..0a145c2 100644 --- a/IdentityShroud.Api/Program.cs +++ b/IdentityShroud.Api/Program.cs @@ -38,19 +38,15 @@ void ConfigureBuilder(WebApplicationBuilder builder) services.AddScoped(); services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddOptions().Bind(configuration.GetSection("db")); services.AddSingleton(); services.AddScoped(); - services.AddScoped(); - services.AddValidatorsFromAssemblyContaining(); - services.AddHttpContextAccessor(); + services.AddValidatorsFromAssemblyContaining(); builder.Host.UseSerilog((context, services, configuration) => configuration .Enrich.FromLogContext() diff --git a/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs b/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs index 844d4ca..85c2fbe 100644 --- a/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs +++ b/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging.Abstractions; +using DotNet.Testcontainers.Containers; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Npgsql; using Testcontainers.PostgreSql; diff --git a/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs b/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs deleted file mode 100644 index 923a865..0000000 --- a/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -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/IdentityShroud.Core.Tests.csproj b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj index 8af08c1..40c87d5 100644 --- a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj +++ b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj @@ -30,4 +30,8 @@ + + + + \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs index bf4d0a6..0fb0a42 100644 --- a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs +++ b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs @@ -72,8 +72,8 @@ public class JwtSignatureGeneratorTests var rsa = RSA.Create(); var parameters = new RSAParameters { - Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus!), - Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent!) + Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus), + Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent) }; rsa.ImportParameters(parameters); diff --git a/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs new file mode 100644 index 0000000..6392676 --- /dev/null +++ b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs @@ -0,0 +1,21 @@ +using System.Security.Cryptography; +using System.Text; +using IdentityShroud.Core.Security; + +namespace IdentityShroud.Core.Tests.Security; + +public class AesGcmHelperTests +{ + [Fact] + public void EncryptDecryptCycleWorks() + { + string input = "Hello, world!"; + + var encryptionKey = RandomNumberGenerator.GetBytes(32); + + var cypher = AesGcmHelper.EncryptAesGcm(Encoding.UTF8.GetBytes(input), encryptionKey); + var output = AesGcmHelper.DecryptAesGcm(cypher, encryptionKey); + + Assert.Equal(input, Encoding.UTF8.GetString(output)); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs b/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs deleted file mode 100644 index 01851a4..0000000 --- a/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text; -using IdentityShroud.Core.Security; -using Microsoft.Extensions.Configuration; - -namespace IdentityShroud.Core.Tests.Security; - -public class ConfigurationSecretProviderTests -{ - private static IConfiguration BuildConfigFromJson(string json) - { - // Convert the JSON string into a stream that the config builder can read. - var jsonBytes = Encoding.UTF8.GetBytes(json); - using var stream = new MemoryStream(jsonBytes); - - // Build the configuration just like the real app does, but from the stream. - var config = new ConfigurationBuilder() - .AddJsonStream(stream) // <-- reads from the in‑memory JSON - .Build(); - - return config; - } - - [Fact] - public void Test() - { - string jsonConfig = """ - { - "secrets": { - "master": [ - { - "Id": "5676d159-5495-4945-aa84-59ee694aa8a2", - "Active": true, - "Algorithm": "AES", - "Key": "yoQ4W7EaNjo7s3FBYkWo5BLyX1BnLyWd7BlSaDIrkzo=" - }, - { - "Id": "b82489e7-a05a-4d64-b9a5-58d2f2c0dc39", - "Active": false, - "Algorithm": "AES", - "Key": "YSWK6vTJXCJOGLpCo+TtZ6anKNzvA1VT2xXLHbmq4M0=" - } - ] - } - } - """; - - - ConfigurationSecretProvider sut = new(BuildConfigFromJson(jsonConfig)); - - // act - var keys = sut.GetKeys("master"); - - // verify - Assert.Equal(2, keys.Length); - var active = keys.Single(k => k.Active); - Assert.Equal(new Guid("5676d159-5495-4945-aa84-59ee694aa8a2"), active.Id.Id); - Assert.Equal("AES", active.Algorithm); - Assert.Equal(Convert.FromBase64String("yoQ4W7EaNjo7s3FBYkWo5BLyX1BnLyWd7BlSaDIrkzo="), active.Key); - - var inactive = keys.Single(k => !k.Active); - Assert.Equal(new Guid("b82489e7-a05a-4d64-b9a5-58d2f2c0dc39"), inactive.Id.Id); - } -} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs b/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs index d0269e6..cb2e772 100644 --- a/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs +++ b/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs @@ -10,8 +10,7 @@ namespace IdentityShroud.Core.Tests.Services; public class ClientServiceTests : IClassFixture { private readonly DbFixture _dbFixture; - private readonly NullDataEncryptionService _dataEncryptionService = new(); - + private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); private readonly IClock _clock = Substitute.For(); private readonly Guid _realmId = new("a1b2c3d4-0000-0000-0000-000000000001"); @@ -52,7 +51,7 @@ public class ClientServiceTests : IClassFixture await using (var db = _dbFixture.CreateDbContext()) { // Act - ClientService sut = new(db, _dataEncryptionService, _clock); + ClientService sut = new(db, _encryptionService, _clock); var response = await sut.Create( _realmId, new ClientCreateRequest @@ -108,8 +107,8 @@ public class ClientServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - ClientService sut = new(actContext, _dataEncryptionService, _clock); - Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken); + ClientService sut = new(actContext, _encryptionService, _clock); + Client? result = await sut.GetByClientId(clientId, TestContext.Current.CancellationToken); // Verify if (shouldFind) @@ -143,8 +142,8 @@ public class ClientServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - ClientService sut = new(actContext, _dataEncryptionService, _clock); - Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken); + ClientService sut = new(actContext, _encryptionService, _clock); + Client? result = await sut.FindById(searchId, TestContext.Current.CancellationToken); // Verify if (shouldFind) diff --git a/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs deleted file mode 100644 index 4f88e48..0000000 --- a/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -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.Tests/Services/DekEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs deleted file mode 100644 index fc4a45f..0000000 --- a/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; -using IdentityShroud.Core.Services; - -namespace IdentityShroud.Core.Tests.Services; - -public class DekEncryptionServiceTests -{ - [Fact] - public void RoundtripWorks() - { - // Note this code will tend to only test the latest verion. - - // setup - byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw="); - var secretProvider = Substitute.For(); - KeyEncryptionKey[] keys = - [ - new KeyEncryptionKey(KekId.NewId(), true, "AES", keyValue) - ]; - secretProvider.GetKeys("master").Returns(keys); - - - ReadOnlySpan input = "Hello, World!"u8; - - // act - DekEncryptionService sut = new(secretProvider); - EncryptedDek cipher = sut.Encrypt(input.ToArray()); - byte[] result = sut.Decrypt(cipher); - - // verify - Assert.Equal(input, 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 - ]; - EncryptedDek secret = new(kid, cipher); - - byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw="); - var secretProvider = Substitute.For(); - KeyEncryptionKey[] keys = - [ - new KeyEncryptionKey(kid, true, "AES", keyValue) - ]; - secretProvider.GetKeys("master").Returns(keys); - - // act - DekEncryptionService sut = new(secretProvider); - Assert.Throws( - () => sut.Decrypt(secret), - ex => ex.Message.Contains("Decryption failed") ? null : "Expected Decryption failed in message"); - } - - [Fact] - public void DecodeSelectsRightKey() - { - // 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 - ]; - EncryptedDek secret = new(kid1, cipher); - - byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw="); - byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw="); - var secretProvider = Substitute.For(); - KeyEncryptionKey[] keys = - [ - new KeyEncryptionKey(kid2, true, "AES", keyValue2), - new KeyEncryptionKey(kid1, false, "AES", keyValue1), - ]; - secretProvider.GetKeys("master").Returns(keys); - - // act - DekEncryptionService sut = new(secretProvider); - byte[] result = sut.Decrypt(secret); - - // verify - Assert.Equal("Hello, World!"u8, result); - } - - [Fact] - 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(); - KeyEncryptionKey[] keys = - [ - new KeyEncryptionKey(kid1, false, "AES", keyValue1), - new KeyEncryptionKey(kid2, true, "AES", keyValue2), - ]; - secretProvider.GetKeys("master").Returns(keys); - - ReadOnlySpan input = "Hello, World!"u8; - // act - DekEncryptionService sut = new(secretProvider); - EncryptedDek cipher = sut.Encrypt(input.ToArray()); - - // Verify - Assert.Equal(kid2, cipher.KekId); - } -} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs new file mode 100644 index 0000000..b855732 --- /dev/null +++ b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Services; + +namespace IdentityShroud.Core.Tests.Services; + +public class EncryptionServiceTests +{ + [Fact] + public void RoundtripWorks() + { + // setup + string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); + var secretProvider = Substitute.For(); + secretProvider.GetSecret("Master").Returns(key); + + EncryptionService sut = new(secretProvider); + byte[] input = RandomNumberGenerator.GetBytes(16); + + // act + var cipher = sut.Encrypt(input); + var result = sut.Decrypt(cipher); + + Assert.Equal(input, result); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionTests.cs deleted file mode 100644 index 2dfbb52..0000000 --- a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -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); - } - - -} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs b/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs index fda233e..60764bc 100644 --- a/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs +++ b/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs @@ -1,9 +1,9 @@ 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; +using IdentityShroud.TestUtils.Substitutes; using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Tests.Services; @@ -40,13 +40,7 @@ public class RealmServiceTests : IClassFixture await using (var db = _dbFixture.CreateDbContext()) { _keyService.CreateKey(Arg.Any()) - .Returns(new RealmKey() - { - Id = Guid.NewGuid(), - KeyType = "TST", - Key = new(KekId.NewId(), [21]), - CreatedAt = DateTime.UtcNow - }); + .Returns(new RealmKey(Guid.NewGuid(), "TST", [21], DateTime.UtcNow)); // Act RealmService sut = new(db, _keyService); var response = await sut.Create( diff --git a/IdentityShroud.Core.Tests/UnitTest1.cs b/IdentityShroud.Core.Tests/UnitTest1.cs index 7506fd0..2d28047 100644 --- a/IdentityShroud.Core.Tests/UnitTest1.cs +++ b/IdentityShroud.Core.Tests/UnitTest1.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using IdentityShroud.Core.DTO; +using IdentityShroud.Core.Messages; using Microsoft.AspNetCore.WebUtilities; namespace IdentityShroud.Core.Tests; @@ -35,6 +36,7 @@ public class UnitTest1 // Option 3: Generate a new key for testing rsa.KeySize = 2048; + // Your already encoded header and payload string header = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0"; string payload = "eyJleHAiOjE3Njk5MzY5MDksImlhdCI6MTc2OTkzNjYwOSwianRpIjoiMjNiZDJmNjktODdhYi00YmM2LWE0MWQtZGZkNzkxNDc4ZDM0IiwiaXNzIjoiaHR0cHM6Ly9pYW0ua2Fzc2FjbG91ZC5ubC9hdXRoL3JlYWxtcy9tcGx1c2thc3NhIiwiYXVkIjpbImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSIsImFwYWNoZTItaW50cmFuZXQtYXV0aCIsImFjY291bnQiXSwic3ViIjoiMDkzY2NmMTUtYzRhOS00YWI0LTk3MWYtZDVhMDIyMzZkODVhIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibXBvYmFja2VuZCIsInNpZCI6IjI2NmUyNjJiLTU5NjMtNDUyZi04ZTI3LWIwZTkzMjBkNTZkNiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1wbHVza2Fzc2EiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVhbGVyLW1lZGV3ZXJrZXItcm9sZSIsIm1wbHVza2Fzc2EtbWVkZXdlcmtlci1yb2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIjp7InJvbGVzIjpbImludHJhbmV0IiwicmVsZWFzZW5vdGVzX3dyaXRlIl19LCJrYXNzYS1tYW5hZ2VtZW50LXNlcnZpY2UiOnsicm9sZXMiOlsicG9zYWNjb3VudF9wYXNzd29yZHJlc2V0IiwiZHJhZnRfbGljZW5zZV93cml0ZSIsImxpY2Vuc2VfcmVhZCIsImtub3dsZWRnZUl0ZW1fcmVhZCIsIm1haWxpbmdfcmVhZCIsIm1wbHVzYXBpX3JlYWQiLCJkYXRhYmFzZV91c2VyX3dyaXRlIiwiZW52aXJvbm1lbnRfd3JpdGUiLCJna3NfYXV0aGNvZGVfcmVhZCIsImVtcGxveWVlX3JlYWQiLCJkYXRhYmFzZV91c2VyX3JlYWQiLCJhcGlhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJtcGx1c2FwaV93cml0ZSIsImVudmlyb25tZW50X3JlYWQiLCJrbm93bGVkZ2VJdGVtX3dyaXRlIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9yZWFkIiwibGljZW5zZV93cml0ZSIsImN1c3RvbWVyX3dyaXRlIiwiZGVhbGVyX3JlYWQiLCJlbXBsb3llZV93cml0ZSIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fd3JpdGUiLCJyZWxhdGlvbnNfcmVhZCIsImRhdGFiYXNlX3VzZXJfcGFzc3dvcmRfbXBsdXNfZW5jcnlwdGVkX3JlYWQiLCJkcmFmdF9saWNlbnNlX3JlYWQiLCJkYXRhYmFzZV9jb25maWd1cmF0aW9uX3JlYWQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoia21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9"; @@ -50,15 +52,6 @@ public class UnitTest1 // Or generate complete JWT // string completeJwt = JwtSignatureGenerator.GenerateCompleteJwt(header, payload, rsa); // Console.WriteLine($"Complete JWT: {completeJwt}"); - - rsa.ExportRSAPublicKey(); // PKCS#1 - } - - using (ECDsa dsa = ECDsa.Create()) - { - dsa.ExportPkcs8PrivateKey(); - - dsa.ExportSubjectPublicKeyInfo(); // x509 } } } @@ -74,9 +67,9 @@ public static class JwtReader return new JsonWebToken() { Header = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot)))!, + Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot))), Payload = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1))))!, + Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1)))), Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1)) }; } diff --git a/IdentityShroud.Core/Contracts/IClientService.cs b/IdentityShroud.Core/Contracts/IClientService.cs index 20e270c..15c0eba 100644 --- a/IdentityShroud.Core/Contracts/IClientService.cs +++ b/IdentityShroud.Core/Contracts/IClientService.cs @@ -2,6 +2,18 @@ using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Contracts; +//public record CreateClientRequest(Guid RealmId, string ClientId, string? Description); + +public class ClientCreateRequest +{ + public required string ClientId { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? SignatureAlgorithm { get; set; } + public bool? AllowClientCredentialsFlow { get; set; } +} + + public interface IClientService { Task> Create( @@ -9,6 +21,6 @@ public interface IClientService ClientCreateRequest request, CancellationToken ct = default); - Task GetByClientId(Guid realmId, string clientId, CancellationToken ct = default); - Task FindById(Guid realmId, int id, CancellationToken ct = default); + Task GetByClientId(string clientId, CancellationToken ct = default); + Task FindById(int id, CancellationToken ct = default); } \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IDataEncryptionService.cs b/IdentityShroud.Core/Contracts/IDataEncryptionService.cs deleted file mode 100644 index 2810aaa..0000000 --- a/IdentityShroud.Core/Contracts/IDataEncryptionService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using IdentityShroud.Core.Security; - -namespace IdentityShroud.Core.Contracts; - -public interface IDataEncryptionService -{ - EncryptedValue Encrypt(ReadOnlySpan plain); - byte[] Decrypt(EncryptedValue input); -} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs b/IdentityShroud.Core/Contracts/IDekEncryptionService.cs deleted file mode 100644 index 3032040..0000000 --- a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using IdentityShroud.Core.Security; - -namespace IdentityShroud.Core.Contracts; - - - -public interface IDekEncryptionService -{ - EncryptedDek Encrypt(ReadOnlySpan plain); - byte[] Decrypt(EncryptedDek input); -} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IEncryptionService.cs b/IdentityShroud.Core/Contracts/IEncryptionService.cs new file mode 100644 index 0000000..a737732 --- /dev/null +++ b/IdentityShroud.Core/Contracts/IEncryptionService.cs @@ -0,0 +1,7 @@ +namespace IdentityShroud.Core.Contracts; + +public interface IEncryptionService +{ + byte[] Encrypt(byte[] plain); + byte[] Decrypt(ReadOnlyMemory cipher); +} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IRealmContext.cs b/IdentityShroud.Core/Contracts/IRealmContext.cs deleted file mode 100644 index c757a02..0000000 --- a/IdentityShroud.Core/Contracts/IRealmContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using IdentityShroud.Core.Model; - -namespace IdentityShroud.Core.Contracts; - -public interface IRealmContext -{ - public Realm GetRealm(); - Task> GetDeks(CancellationToken ct = default); -} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IRealmService.cs b/IdentityShroud.Core/Contracts/IRealmService.cs index 4598b97..b740aa5 100644 --- a/IdentityShroud.Core/Contracts/IRealmService.cs +++ b/IdentityShroud.Core/Contracts/IRealmService.cs @@ -11,5 +11,4 @@ public interface IRealmService Task> Create(RealmCreateRequest request, CancellationToken ct = default); Task LoadActiveKeys(Realm realm); - Task LoadDeks(Realm realm); } \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/ISecretProvider.cs b/IdentityShroud.Core/Contracts/ISecretProvider.cs index 4d4182e..2a8e9e6 100644 --- a/IdentityShroud.Core/Contracts/ISecretProvider.cs +++ b/IdentityShroud.Core/Contracts/ISecretProvider.cs @@ -1,14 +1,6 @@ -using IdentityShroud.Core.Security; - namespace IdentityShroud.Core.Contracts; public interface ISecretProvider { string GetSecret(string name); - - /// - /// Should return one active key, might return inactive keys. - /// - /// - KeyEncryptionKey[] GetKeys(string name); } diff --git a/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs b/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs deleted file mode 100644 index a162131..0000000 --- a/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace IdentityShroud.Core.Contracts; - -public class ClientCreateRequest -{ - public required string ClientId { get; set; } - public string? Name { get; set; } - public string? Description { get; set; } - public string? SignatureAlgorithm { get; set; } - public bool? AllowClientCredentialsFlow { get; set; } -} \ No newline at end of file diff --git a/IdentityShroud.Core/DTO/JsonWebKey.cs b/IdentityShroud.Core/DTO/JsonWebKey.cs index 4f16955..ea4d7d5 100644 --- a/IdentityShroud.Core/DTO/JsonWebKey.cs +++ b/IdentityShroud.Core/DTO/JsonWebKey.cs @@ -1,5 +1,7 @@ +using System.Buffers; +using System.Buffers.Text; +using System.Text.Json; using System.Text.Json.Serialization; -using IdentityShroud.Core.Helpers; namespace IdentityShroud.Core.Messages; @@ -46,4 +48,26 @@ 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/Db.cs b/IdentityShroud.Core/Db.cs index a37136c..cd7a493 100644 --- a/IdentityShroud.Core/Db.cs +++ b/IdentityShroud.Core/Db.cs @@ -1,7 +1,5 @@ using IdentityShroud.Core.Model; -using IdentityShroud.Core.Security; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,41 +19,7 @@ public class Db( public virtual DbSet Clients { get; set; } public virtual DbSet Realms { get; set; } public virtual DbSet Keys { get; set; } - public virtual DbSet Deks { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var dekIdConverter = new ValueConverter( - id => id.Id, - guid => new DekId(guid)); - - var kekIdConverter = new ValueConverter( - id => id.Id, - guid => new KekId(guid)); - - modelBuilder.Entity() - .Property(d => d.Id) - .HasConversion(dekIdConverter); - - modelBuilder.Entity() - .OwnsOne(d => d.KeyData, keyData => - { - keyData.Property(k => k.KekId).HasConversion(kekIdConverter); - }); - - modelBuilder.Entity() - .OwnsOne(k => k.Key, key => - { - key.Property(k => k.KekId).HasConversion(kekIdConverter); - }); - - modelBuilder.Entity() - .OwnsOne(c => c.Secret, secret => - { - secret.Property(s => s.DekId).HasConversion(dekIdConverter); - }); - } - + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(""); diff --git a/IdentityShroud.Core/Helpers/Base64UrlConverter.cs b/IdentityShroud.Core/Helpers/Base64UrlConverter.cs deleted file mode 100644 index 77f05f2..0000000 --- a/IdentityShroud.Core/Helpers/Base64UrlConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -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/Helpers/SlugHelper.cs b/IdentityShroud.Core/Helpers/SlugHelper.cs index 51aa0c3..beef894 100644 --- a/IdentityShroud.Core/Helpers/SlugHelper.cs +++ b/IdentityShroud.Core/Helpers/SlugHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using System.Security.Cryptography; using System.Text; diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj b/IdentityShroud.Core/IdentityShroud.Core.csproj index 9dd3e34..d9d6809 100644 --- a/IdentityShroud.Core/IdentityShroud.Core.csproj +++ b/IdentityShroud.Core/IdentityShroud.Core.csproj @@ -12,9 +12,7 @@ - - @@ -22,4 +20,10 @@ + + + ..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll + + + diff --git a/IdentityShroud.Core/Model/Client.cs b/IdentityShroud.Core/Model/Client.cs index 5df6c1a..a8c9e29 100644 --- a/IdentityShroud.Core/Model/Client.cs +++ b/IdentityShroud.Core/Model/Client.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using IdentityShroud.Core.Security; using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Model; diff --git a/IdentityShroud.Core/Model/ClientSecret.cs b/IdentityShroud.Core/Model/ClientSecret.cs index 52d25cc..bd57d37 100644 --- a/IdentityShroud.Core/Model/ClientSecret.cs +++ b/IdentityShroud.Core/Model/ClientSecret.cs @@ -1,7 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; namespace IdentityShroud.Core.Model; @@ -13,5 +11,5 @@ public class ClientSecret public Guid ClientId { get; set; } public DateTime CreatedAt { get; set; } public DateTime? RevokedAt { get; set; } - public required EncryptedValue Secret { get; set; } + public required byte[] SecretEncrypted { get; set; } } \ No newline at end of file diff --git a/IdentityShroud.Core/Model/Realm.cs b/IdentityShroud.Core/Model/Realm.cs index bbe9631..c02fc38 100644 --- a/IdentityShroud.Core/Model/Realm.cs +++ b/IdentityShroud.Core/Model/Realm.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using IdentityShroud.Core.Security; +using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Model; @@ -21,20 +22,9 @@ public class Realm public List Keys { get; init; } = []; - public List Deks { get; init; } = []; - /// /// Can be overriden per client /// public string DefaultSignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256; -} - -[Table("realm_dek")] -public record RealmDek -{ - public required DekId Id { 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; } + } diff --git a/IdentityShroud.Core/Model/RealmKey.cs b/IdentityShroud.Core/Model/RealmKey.cs index 3fcf2d1..14c7c9c 100644 --- a/IdentityShroud.Core/Model/RealmKey.cs +++ b/IdentityShroud.Core/Model/RealmKey.cs @@ -1,20 +1,15 @@ using System.ComponentModel.DataAnnotations.Schema; -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; -using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Model; [Table("realm_key")] -public record RealmKey +public record RealmKey(Guid Id, string KeyType, byte[] KeyDataEncrypted, DateTime CreatedAt) { - public required Guid Id { get; init; } - public required string KeyType { get; init; } - - - public required EncryptedDek Key { get; init; } - public required DateTime CreatedAt { get; init; } + 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 DateTime? RevokedAt { get; set; } /// diff --git a/IdentityShroud.Core/Security/AesGcmHelper.cs b/IdentityShroud.Core/Security/AesGcmHelper.cs new file mode 100644 index 0000000..bfa5809 --- /dev/null +++ b/IdentityShroud.Core/Security/AesGcmHelper.cs @@ -0,0 +1,70 @@ +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 nonce = stackalloc byte[AesGcm.NonceByteSizes.MaxSize]; + RandomNumberGenerator.Fill(nonce); + Span ciphertext = stackalloc byte[plaintext.Length]; + Span 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 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 nonce = payload.Span[..nonceSize]; + ReadOnlySpan ciphertext = payload.Span.Slice(nonceSize, payload.Length - nonceSize - tagSize); + ReadOnlySpan 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; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs b/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs index 9355c0b..ab77ef1 100644 --- a/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs +++ b/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs @@ -14,9 +14,4 @@ public class ConfigurationSecretProvider(IConfiguration configuration) : ISecret { return secrets.GetValue(name) ?? ""; } - - public KeyEncryptionKey[] GetKeys(string name) - { - return secrets.GetSection(name).Get() ?? []; - } } \ No newline at end of file diff --git a/IdentityShroud.Core/Security/DekId.cs b/IdentityShroud.Core/Security/DekId.cs deleted file mode 100644 index 276178e..0000000 --- a/IdentityShroud.Core/Security/DekId.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace IdentityShroud.Core.Security; - -public record struct DekId(Guid Id) -{ - public static DekId NewId() => new(Guid.NewGuid()); -} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/EncryptedDek.cs b/IdentityShroud.Core/Security/EncryptedDek.cs deleted file mode 100644 index 377a2f6..0000000 --- a/IdentityShroud.Core/Security/EncryptedDek.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace IdentityShroud.Core.Security; - -[Owned] -public record EncryptedDek(KekId KekId, byte[] Value); \ No newline at end of file diff --git a/IdentityShroud.Core/Security/EncryptedValue.cs b/IdentityShroud.Core/Security/EncryptedValue.cs deleted file mode 100644 index 173c295..0000000 --- a/IdentityShroud.Core/Security/EncryptedValue.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace IdentityShroud.Core.Security; - -[Owned] -public record EncryptedValue(DekId DekId, byte[] Value); - - diff --git a/IdentityShroud.Core/Security/Encryption.cs b/IdentityShroud.Core/Security/Encryption.cs deleted file mode 100644 index 47344c1..0000000 --- a/IdentityShroud.Core/Security/Encryption.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Security.Cryptography; - -namespace IdentityShroud.Core.Security; - -public static class Encryption -{ - private record struct AlgVersion(int Version, int NonceSize, int TagSize); - - private static AlgVersion[] _versions = - [ - new(0, 0, 0), // version 0 does not realy exist - new(1, 12, 16), // version 1 - ]; - - public static byte[] Encrypt(ReadOnlySpan plaintext, ReadOnlySpan key) - { - 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)versionParams.Version; - - // 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(key, versionParams.TagSize); - aes.Encrypt(nonce, plaintext, cipher, tag); - return result; - } - - public static byte[] Decrypt(ReadOnlyMemory input, ReadOnlySpan key) - { - var payload = input.Span; - int versionNumber = (int)payload[0]; - if (versionNumber != 1) - throw new ArgumentException("Invalid payload"); - - 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 nonce = payload.Slice(1, versionParams.NonceSize); - ReadOnlySpan tag = payload.Slice(1 + versionParams.NonceSize, versionParams.TagSize); - ReadOnlySpan cipher = payload.Slice(1 + versionParams.NonceSize + versionParams.TagSize); - - byte[] plaintext = new byte[cipher.Length]; - - using var aes = new AesGcm(key, 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; - } -} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/JsonWebAlgorithm.cs b/IdentityShroud.Core/Security/JsonWebAlgorithm.cs index dc9bc28..cbdcf05 100644 --- a/IdentityShroud.Core/Security/JsonWebAlgorithm.cs +++ b/IdentityShroud.Core/Security/JsonWebAlgorithm.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; + namespace IdentityShroud.Core.Security; public static class JsonWebAlgorithm diff --git a/IdentityShroud.Core/Security/KekId.cs b/IdentityShroud.Core/Security/KekId.cs deleted file mode 100644 index c794078..0000000 --- a/IdentityShroud.Core/Security/KekId.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.ComponentModel; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace IdentityShroud.Core.Security; - -[JsonConverter(typeof(KekIdJsonConverter))] -[TypeConverter(typeof(KekIdTypeConverter))] -public readonly record struct KekId -{ - public Guid Id { get; } - - public KekId(Guid id) - { - Id = id; - } - - public static KekId NewId() - { - return new KekId(Guid.NewGuid()); - } -} - -public class KekIdJsonConverter : JsonConverter -{ - public override KekId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => new KekId(reader.GetGuid()); - - public override void Write(Utf8JsonWriter writer, KekId value, JsonSerializerOptions options) - => writer.WriteStringValue(value.Id); -} - -public class KekIdTypeConverter : TypeConverter -{ - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - => value is string s ? new KekId(Guid.Parse(s)) : base.ConvertFrom(context, culture, value); -} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/KeyEncryptionKey.cs b/IdentityShroud.Core/Security/KeyEncryptionKey.cs deleted file mode 100644 index 35f7917..0000000 --- a/IdentityShroud.Core/Security/KeyEncryptionKey.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace IdentityShroud.Core.Security; - -/// -/// Contains a KEK and associated relevant data. This structure -/// -/// -/// -/// -/// -public record KeyEncryptionKey(KekId Id, bool Active, string Algorithm, byte[] Key); diff --git a/IdentityShroud.Core/Security/Keys/IKeyProvider.cs b/IdentityShroud.Core/Security/Keys/IKeyProvider.cs index 8e32309..ec095b5 100644 --- a/IdentityShroud.Core/Security/Keys/IKeyProvider.cs +++ b/IdentityShroud.Core/Security/Keys/IKeyProvider.cs @@ -1,4 +1,5 @@ using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Security.Keys; diff --git a/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs b/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs index daf2b7f..a5bcee8 100644 --- a/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs +++ b/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs @@ -1,6 +1,8 @@ using System.Buffers.Text; using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Security.Keys.Rsa; diff --git a/IdentityShroud.Core/Security/RsaHelper.cs b/IdentityShroud.Core/Security/RsaHelper.cs new file mode 100644 index 0000000..ab49ebd --- /dev/null +++ b/IdentityShroud.Core/Security/RsaHelper.cs @@ -0,0 +1,16 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.Core.Security; + +public static class RsaHelper +{ + /// + /// Load RSA private key from PKCS#8 format + /// + public static RSA LoadFromPkcs8(byte[] pkcs8Key) + { + var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(pkcs8Key, out _); + return rsa; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/ClientService.cs b/IdentityShroud.Core/Services/ClientService.cs index 0887ccd..2e556d4 100644 --- a/IdentityShroud.Core/Services/ClientService.cs +++ b/IdentityShroud.Core/Services/ClientService.cs @@ -7,7 +7,7 @@ namespace IdentityShroud.Core.Services; public class ClientService( Db db, - IDataEncryptionService cryptor, + IEncryptionService cryptor, IClock clock) : IClientService { public async Task> Create(Guid realmId, ClientCreateRequest request, CancellationToken ct = default) @@ -34,31 +34,24 @@ public class ClientService( return client; } - public async Task GetByClientId( - Guid realmId, - string clientId, - CancellationToken ct = default) + public async Task GetByClientId(string clientId, CancellationToken ct = default) { - return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.RealmId == realmId, ct); + return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId, ct); } - public async Task FindById( - Guid realmId, - int id, - CancellationToken ct = default) + public async Task FindById(int id, CancellationToken ct = default) { - return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct); + return await db.Clients.FirstOrDefaultAsync(c => c.Id == id, ct); } private ClientSecret CreateSecret() { - Span secret = stackalloc byte[24]; - RandomNumberGenerator.Fill(secret); + byte[] secret = RandomNumberGenerator.GetBytes(24); return new ClientSecret() { CreatedAt = clock.UtcNow(), - Secret = cryptor.Encrypt(secret.ToArray()), + SecretEncrypted = cryptor.Encrypt(secret), }; } diff --git a/IdentityShroud.Core/Services/DataEncryptionService.cs b/IdentityShroud.Core/Services/DataEncryptionService.cs deleted file mode 100644 index a06cbae..0000000 --- a/IdentityShroud.Core/Services/DataEncryptionService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Model; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.Core.Services; - -public class DataEncryptionService( - IRealmContext realmContext, - IDekEncryptionService dekCryptor) : IDataEncryptionService -{ - - // 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 IList? _deks = null; - - private IList GetDeks() - { - if (_deks is null) - _deks = realmContext.GetDeks().Result; - - return _deks; - } - - private RealmDek GetActiveDek() => GetDeks().Single(d => d.Active); - private RealmDek GetKey(DekId id) => GetDeks().Single(d => d.Id == id); - - public byte[] Decrypt(EncryptedValue input) - { - var dek = GetKey(input.DekId); - var key = dekCryptor.Decrypt(dek.KeyData); - return Encryption.Decrypt(input.Value, key); - } - - public EncryptedValue Encrypt(ReadOnlySpan plain) - { - var dek = GetActiveDek(); - var key = dekCryptor.Decrypt(dek.KeyData); - byte[] cipher = Encryption.Encrypt(plain, key); - return new (dek.Id, cipher); - } -} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/DekEncryptionService.cs b/IdentityShroud.Core/Services/DekEncryptionService.cs deleted file mode 100644 index add9267..0000000 --- a/IdentityShroud.Core/Services/DekEncryptionService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.Core.Services; - -/// -/// -/// -public class DekEncryptionService : IDekEncryptionService -{ - // 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 KeyEncryptionKey[] _encryptionKeys; - - private KeyEncryptionKey ActiveKey => _encryptionKeys.Single(k => k.Active); - private KeyEncryptionKey GetKey(KekId keyId) => _encryptionKeys.Single(k => k.Id == keyId); - - public DekEncryptionService(ISecretProvider secretProvider) - { - _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 EncryptedDek Encrypt(ReadOnlySpan plaintext) - { - var encryptionKey = ActiveKey; - byte[] cipher = Encryption.Encrypt(plaintext, encryptionKey.Key); - return new (encryptionKey.Id, cipher); - } - - public byte[] Decrypt(EncryptedDek input) - { - var encryptionKey = GetKey(input.KekId); - - return Encryption.Decrypt(input.Value, encryptionKey.Key); - } -} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/EncryptionService.cs b/IdentityShroud.Core/Services/EncryptionService.cs new file mode 100644 index 0000000..a4455e0 --- /dev/null +++ b/IdentityShroud.Core/Services/EncryptionService.cs @@ -0,0 +1,27 @@ +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Security; + +namespace IdentityShroud.Core.Services; + +/// +/// +/// +public class EncryptionService : IEncryptionService +{ + private readonly byte[] encryptionKey; + + public EncryptionService(ISecretProvider secretProvider) + { + encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master")); + } + + public byte[] Encrypt(byte[] plain) + { + return AesGcmHelper.EncryptAesGcm(plain, encryptionKey); + } + + public byte[] Decrypt(ReadOnlyMemory cipher) + { + return AesGcmHelper.DecryptAesGcm(cipher, encryptionKey); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/KeyService.cs b/IdentityShroud.Core/Services/KeyService.cs index a2ce9dc..440dff9 100644 --- a/IdentityShroud.Core/Services/KeyService.cs +++ b/IdentityShroud.Core/Services/KeyService.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; using IdentityShroud.Core.Model; @@ -6,7 +7,7 @@ using IdentityShroud.Core.Security.Keys; namespace IdentityShroud.Core.Services; public class KeyService( - IDekEncryptionService cryptor, + IEncryptionService cryptor, IKeyProviderFactory keyProviderFactory, IClock clock) : IKeyService { @@ -29,18 +30,23 @@ public class KeyService( IKeyProvider provider = keyProviderFactory.CreateProvider(realmKey.KeyType); provider.SetJwkParameters( - cryptor.Decrypt(realmKey.Key), + cryptor.Decrypt(realmKey.KeyDataEncrypted), jwk); return jwk; } private RealmKey CreateKey(string keyType, byte[] plainKey) => - new RealmKey() - { - Id = Guid.NewGuid(), - KeyType = keyType, - Key = cryptor.Encrypt(plainKey), - CreatedAt = clock.UtcNow(), - }; + 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; + // } } diff --git a/IdentityShroud.Core/Services/RealmContext.cs b/IdentityShroud.Core/Services/RealmContext.cs deleted file mode 100644 index 7daa399..0000000 --- a/IdentityShroud.Core/Services/RealmContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Model; -using Microsoft.AspNetCore.Http; - -namespace IdentityShroud.Core.Services; - -public class RealmContext( - IHttpContextAccessor accessor, - IRealmService realmService) : IRealmContext -{ - public Realm GetRealm() - { - return (Realm)accessor.HttpContext.Items["RealmEntity"]; - } - - public async Task> GetDeks(CancellationToken ct = default) - { - Realm realm = GetRealm(); - if (realm.Deks.Count == 0) - { - await realmService.LoadDeks(realm); - } - - return realm.Deks; - } -} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/RealmService.cs b/IdentityShroud.Core/Services/RealmService.cs index 949c9fe..5385658 100644 --- a/IdentityShroud.Core/Services/RealmService.cs +++ b/IdentityShroud.Core/Services/RealmService.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Helpers; using IdentityShroud.Core.Messages.Realm; @@ -58,12 +59,6 @@ public class RealmService( .Query() .Where(k => k.RevokedAt == null) .LoadAsync(); - } - - public async Task LoadDeks(Realm realm) - { - await db.Entry(realm).Collection(r => r.Deks) - .Query() - .LoadAsync(); + } } \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs b/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs index 016f358..3352bc6 100644 --- a/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs +++ b/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Xunit; namespace IdentityShroud.TestUtils.Asserts; diff --git a/IdentityShroud.TestUtils/Asserts/ResultAssert.cs b/IdentityShroud.TestUtils/Asserts/ResultAssert.cs index ff00c06..28a0b11 100644 --- a/IdentityShroud.TestUtils/Asserts/ResultAssert.cs +++ b/IdentityShroud.TestUtils/Asserts/ResultAssert.cs @@ -1,4 +1,5 @@ using FluentResults; +using Xunit; namespace IdentityShroud.Core.Tests; diff --git a/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj b/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj index 4b68445..0b8cba9 100644 --- a/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj +++ b/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj @@ -10,7 +10,6 @@ - @@ -22,4 +21,10 @@ + + + ..\..\..\.nuget\packages\nsubstitute\5.3.0\lib\net6.0\NSubstitute.dll + + + diff --git a/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs b/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs new file mode 100644 index 0000000..5a81240 --- /dev/null +++ b/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs @@ -0,0 +1,18 @@ +using IdentityShroud.Core.Contracts; + +namespace IdentityShroud.TestUtils.Substitutes; + +public static class EncryptionServiceSubstitute +{ + public static IEncryptionService CreatePassthrough() + { + var encryptionService = Substitute.For(); + encryptionService + .Encrypt(Arg.Any()) + .Returns(x => x.ArgAt(0)); + encryptionService + .Decrypt(Arg.Any>()) + .Returns(x => x.ArgAt>(0).ToArray()); + return encryptionService; + } +} \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs b/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs deleted file mode 100644 index 4e97bfc..0000000 --- a/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.TestUtils.Substitutes; - -public class NullDataEncryptionService : IDataEncryptionService -{ - public DekId KeyId { get; } = DekId.NewId(); - public EncryptedValue Encrypt(ReadOnlySpan plain) - { - return new(KeyId, plain.ToArray()); - } - - public byte[] Decrypt(EncryptedValue input) - { - return input.Value; - } -} \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs b/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs deleted file mode 100644 index 879f932..0000000 --- a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.TestUtils.Substitutes; - -public class NullDekEncryptionService : IDekEncryptionService -{ - public KekId KeyId { get; } = KekId.NewId(); - public EncryptedDek Encrypt(ReadOnlySpan plain) - { - return new(KeyId, plain.ToArray()); - } - - public byte[] Decrypt(EncryptedDek input) - { - return input.Value; - } -} \ No newline at end of file diff --git a/IdentityShroud.sln.DotSettings.user b/IdentityShroud.sln.DotSettings.user index 88c8f46..e39022f 100644 --- a/IdentityShroud.sln.DotSettings.user +++ b/IdentityShroud.sln.DotSettings.user @@ -2,10 +2,8 @@ ForceIncluded ForceIncluded ForceIncluded - ForceIncluded ForceIncluded ForceIncluded - ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -16,19 +14,20 @@ ForceIncluded ForceIncluded ForceIncluded - ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded /home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr - /home/eelke/.dotnet/dotnet /home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Junie Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <ProjectFile>DC887623-8680-4D3B-B23A-D54F7DA91891/d:Services/f:ClientServiceTests.cs</ProjectFile> +</SessionState> @@ -37,12 +36,4 @@ - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 8bd5aa3..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# IdentityShroud - -IdentityShroud is a .NET project for identity management and protection. -