From 054754f553889d10b03dd0a8a55198b11181911b Mon Sep 17 00:00:00 2001 From: eelke Date: Tue, 18 Aug 2026 07:40:24 +0200 Subject: [PATCH] Reworked encryption to use less heap allocated buffers for secrets. Also some work on plugin system. --- .editorconfig | 2 + .../Apis/ClientApiTests.cs | 6 +- .../Fixtures/ApplicationFactory.cs | 1 + .../IdentityShroud.Api.Tests.csproj | 2 +- .../Apis/Mappers/ClientMapper.cs | 3 + IdentityShroud.Api/Apis/OpenIdEndpoints.cs | 2 +- .../AppJsonSerializerContext.cs | 14 - IdentityShroud.Api/IdentityShroud.Api.csproj | 3 +- IdentityShroud.Api/Program.cs | 124 ++++--- .../Properties/launchSettings.json | 2 +- .../Security/Jwt/RsaJwtSignerTests.cs | 48 +++ .../Services/DekEncryptionServiceTests.cs | 14 +- .../Services/EncryptionTests.cs | 3 +- .../Contracts/IDekEncryptionService.cs | 4 +- .../CoreServiceCollectionExtensions.cs | 38 +++ .../IdentityShroud.Core.csproj | 5 + .../Model/DecryptedSigningKey.cs | 66 ++++ IdentityShroud.Core/Plugins/PluginLoader.cs | 59 ++++ IdentityShroud.Core/Plugins/PluginRegistry.cs | 18 + IdentityShroud.Core/Security/Encryption.cs | 39 ++- ...IJwtSignatureProvider.cs => IJwtSigner.cs} | 14 +- .../Security/Jwt/IJwtSignerFactory.cs | 6 + .../Security/Jwt/JwtSignatureGenerator.cs | 55 +-- .../Security/Jwt/JwtSignerFactory.cs | 17 + .../Security/Jwt/RsaJwtSignatureProvider.cs | 88 ----- .../Security/Jwt/RsaJwtSigner.cs | 36 ++ .../Services/DataEncryptionService.cs | 32 +- .../Services/DekEncryptionService.cs | 10 +- .../IdentityShroud.GraphQL.csproj | 17 + IdentityShroud.GraphQL/Query.cs | 26 ++ .../RegistrationExtensions.cs | 31 ++ .../20260412083710_Initial.Designer.cs | 318 ++++++++++++++++++ .../Migrations/20260412083710_Initial.cs | 171 ++++++++++ .../Migrations/DbModelSnapshot.cs | 315 +++++++++++++++++ IdentityShroud.PluginSupport/IPlugin.cs | 9 + .../IdentityShroud.PluginSupport.csproj | 9 + .../ISecretProvider.cs | 15 + .../IdentityShroud.SecretProviders.csproj | 13 + IdentityShroud.SecretProviders/PlainSecret.cs | 18 + .../Substitutes/NullDekEncryptionService.cs | 10 + IdentityShroud.sln | 23 ++ IdentityShroud.sln.DotSettings.user | 8 +- 42 files changed, 1452 insertions(+), 242 deletions(-) create mode 100644 .editorconfig delete mode 100644 IdentityShroud.Api/AppJsonSerializerContext.cs create mode 100644 IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs create mode 100644 IdentityShroud.Core/CoreServiceCollectionExtensions.cs create mode 100644 IdentityShroud.Core/Model/DecryptedSigningKey.cs create mode 100644 IdentityShroud.Core/Plugins/PluginLoader.cs create mode 100644 IdentityShroud.Core/Plugins/PluginRegistry.cs rename IdentityShroud.Core/Security/Jwt/{IJwtSignatureProvider.cs => IJwtSigner.cs} (61%) create mode 100644 IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs create mode 100644 IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs delete mode 100644 IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs create mode 100644 IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs create mode 100644 IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj create mode 100644 IdentityShroud.GraphQL/Query.cs create mode 100644 IdentityShroud.GraphQL/RegistrationExtensions.cs create mode 100644 IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs create mode 100644 IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs create mode 100644 IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs create mode 100644 IdentityShroud.PluginSupport/IPlugin.cs create mode 100644 IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj create mode 100644 IdentityShroud.SecretProviders/ISecretProvider.cs create mode 100644 IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj create mode 100644 IdentityShroud.SecretProviders/PlainSecret.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..33a3ce8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +resharper_naming_rules.abbreviations = QL, DB diff --git a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs b/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs index 7133bd7..cf1eb9f 100644 --- a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs +++ b/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs @@ -17,10 +17,8 @@ namespace IdentityShroud.Api.Tests.Apis; public class ClientApiTests : IClassFixture { - private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web) - { - TypeInfoResolver = AppJsonSerializerContext.Default, - }; + private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + private readonly ApplicationFactory _factory; public ClientApiTests(ApplicationFactory factory) diff --git a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs index 9846559..0c5337d 100644 --- a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs +++ b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs @@ -1,3 +1,4 @@ +using IdentityShroud.Api; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; diff --git a/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj b/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj index a3aa6a8..4bb8f47 100644 --- a/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj +++ b/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs b/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs index 8e58717..0c6563f 100644 --- a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs +++ b/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs @@ -6,6 +6,9 @@ namespace IdentityShroud.Api.Mappers; [Mapper] public partial class ClientMapper { + // skipping secret as we do not have the DEK [MapperIgnoreSource(nameof(Client.Secrets))] + [MapperIgnoreTarget(nameof(ClientRepresentation.Secret))] public partial ClientRepresentation ToDto(Client client); + } \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/OpenIdEndpoints.cs b/IdentityShroud.Api/Apis/OpenIdEndpoints.cs index 053be93..54b972a 100644 --- a/IdentityShroud.Api/Apis/OpenIdEndpoints.cs +++ b/IdentityShroud.Api/Apis/OpenIdEndpoints.cs @@ -47,7 +47,7 @@ public static class OpenIdEndpoints TokenEndpoint = baseUri + "/openid-connect/token", Issuer = baseUri, JwksUri = baseUri + "/openid-connect/jwks", - }, AppJsonSerializerContext.Default.OpenIdConfiguration); + }); } private static async Task, BadRequest>> OpenIdConnectJwks( diff --git a/IdentityShroud.Api/AppJsonSerializerContext.cs b/IdentityShroud.Api/AppJsonSerializerContext.cs deleted file mode 100644 index 5733ac3..0000000 --- a/IdentityShroud.Api/AppJsonSerializerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json.Serialization; -using IdentityShroud.Api.Apis; -using IdentityShroud.Core.Messages; -using IdentityShroud.Core.Messages.Realm; - -namespace IdentityShroud.Api; - -[JsonSerializable(typeof(ClientRepresentation))] -[JsonSerializable(typeof(ErrorDto))] -[JsonSerializable(typeof(OpenIdConfiguration))] -[JsonSerializable(typeof(RealmCreateRequest))] -public partial class AppJsonSerializerContext : JsonSerializerContext -{ -} \ No newline at end of file diff --git a/IdentityShroud.Api/IdentityShroud.Api.csproj b/IdentityShroud.Api/IdentityShroud.Api.csproj index 31f88b2..5d779e7 100644 --- a/IdentityShroud.Api/IdentityShroud.Api.csproj +++ b/IdentityShroud.Api/IdentityShroud.Api.csproj @@ -5,7 +5,7 @@ enable enable true - true + false Linux 6b8ef434-0577-4a3c-8749-6b547d7787c5 @@ -25,6 +25,7 @@ + diff --git a/IdentityShroud.Api/Program.cs b/IdentityShroud.Api/Program.cs index b2a31e8..2ff5fe6 100644 --- a/IdentityShroud.Api/Program.cs +++ b/IdentityShroud.Api/Program.cs @@ -1,78 +1,74 @@ using FluentValidation; -using IdentityShroud.Api; using IdentityShroud.Api.Mappers; -using IdentityShroud.Core.Contracts; +using IdentityShroud.Core; using IdentityShroud.Core.EFCore; -using IdentityShroud.Core.Security; -using IdentityShroud.Core.Security.Keys; -using IdentityShroud.Core.Services; +using IdentityShroud.GraphQL; using Serilog; using Serilog.Formatting.Json; - // Initial logging until we can set it up from Configuration -Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console(new JsonFormatter()) - .CreateLogger(); -var applicationBuilder = WebApplication.CreateSlimBuilder(args); -ConfigureBuilder(applicationBuilder); -var application = applicationBuilder.Build(); -ConfigureApplication(application); -application.Run(); +namespace IdentityShroud.Api; -void ConfigureBuilder(WebApplicationBuilder builder) +public class Program { - var services = builder.Services; - var configuration = builder.Configuration; - - //services.AddControllers(); - services.ConfigureHttpJsonOptions(options => + public static void Main(string[] args) { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, IdentityShroud.Api.AppJsonSerializerContext.Default); - }); + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console(new JsonFormatter()) + .CreateLogger(); - // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi - services.AddOpenApi(); - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddOptions().Bind(configuration.GetSection("db")); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - - services.AddValidatorsFromAssemblyContaining(); - services.AddHttpContextAccessor(); - - services.AddExceptionHandler(); - services.AddProblemDetails(); - - builder.Host.UseSerilog((context, services, configuration) => configuration - .Enrich.FromLogContext() - //.Enrich.With() - .ReadFrom.Configuration(context.Configuration)); -} - -void ConfigureApplication(WebApplication app) -{ - app.UseExceptionHandler(); - if (app.Environment.IsDevelopment()) - { - app.MapOpenApi(); + var applicationBuilder = WebApplication.CreateSlimBuilder(args); + ConfigureBuilder(applicationBuilder); + var application = applicationBuilder.Build(); + ConfigureApplication(application); + application.Run(); } - app.UseSerilogRequestLogging(); - app.MapApis(); - - // app.UseRouting(); - // app.MapControllers(); -} -public partial class Program { } + private static void ConfigureBuilder(WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddOptions().Bind(configuration.GetSection("db")); + + // services.ConfigureHttpJsonOptions(options => + // { + // options.SerializerOptions.TypeInfoResolverChain.Insert(0, IdentityShroud.Api.AppJsonSerializerContext.Default); + // }); + + services.AddScoped(); + + services.AddValidatorsFromAssemblyContaining(); + + services.AddHttpContextAccessor(); + services.AddOpenApi(); + services.AddExceptionHandler(); + services.AddProblemDetails(); + + services + .AddCore() + .AddIdentityShroudGraphQL(); + + builder.Host.UseSerilog((context, services, configuration) => configuration + .Enrich.FromLogContext() + //.Enrich.With() + .ReadFrom.Configuration(context.Configuration)); + } + + private static void ConfigureApplication(WebApplication app) + { + app.UseExceptionHandler(); + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + } + app.UseSerilogRequestLogging(); + app.MapApis(); + app.MapIdentityShroudGraphQL(); + + // app.UseRouting(); + // app.MapControllers(); + } +} \ No newline at end of file diff --git a/IdentityShroud.Api/Properties/launchSettings.json b/IdentityShroud.Api/Properties/launchSettings.json index 9472c5a..8556497 100644 --- a/IdentityShroud.Api/Properties/launchSettings.json +++ b/IdentityShroud.Api/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "todos", + "launchUrl": "graphql", "applicationUrl": "http://localhost:5249", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs b/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs new file mode 100644 index 0000000..13b76ac --- /dev/null +++ b/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs @@ -0,0 +1,48 @@ +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; + +namespace IdentityShroud.Core.Tests.Security.Jwt; + +public class RsaJwtSignerTests +{ + [Fact] + public void Test() + { + // ISecretProvider secretProvider = Substitute.For(); + // RealmSigningKey privateKey = new() + // { + // Id = default, + // KeyType = KeyType.RSA, + // Key = new EncryptedDek(KekId.NewId(), [1]), + // CreatedAt = default, + // RevokedAt = null, + // Priority = 0, + // PublicKeyParameters = null + // }; + DecryptedSigningKey key = new(); + byte[] jwt = []; + + RsaJwtSigner provider = new(); + provider.CalculateSignature(JwtSigAlgName.RS256, key, jwt); + // + // new DekEncryptionService(secretProvider), privateKey, + // JwtSigAlgName.RS256); + } + + [Theory] + [InlineData(1024)] + [InlineData(2048)] + [InlineData(4096)] + public void EstimateKeySizeTests(int keySizeBits) + { + using var rsa = RSA.Create(); + rsa.KeySize = keySizeBits; + byte[] b = rsa.ExportPkcs8PrivateKey(); + int estimate = DecryptedSigningKey.EstimatePkcs8ExportSize(keySizeBits); + Assert.True(b.Length < estimate - 100); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs index fc4a45f..c0b9f38 100644 --- a/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs +++ b/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs @@ -25,8 +25,13 @@ public class DekEncryptionServiceTests // act DekEncryptionService sut = new(secretProvider); + EncryptedDek cipher = sut.Encrypt(input.ToArray()); - byte[] result = sut.Decrypt(cipher); + int decryptedSize = sut.GetDecryptedSize(cipher); + Assert.Equal(input.Length, decryptedSize); + + var result = new byte[decryptedSize]; + sut.Decrypt(cipher, result); // verify Assert.Equal(input, result); @@ -56,8 +61,10 @@ public class DekEncryptionServiceTests // act DekEncryptionService sut = new(secretProvider); + int decryptedSize = sut.GetDecryptedSize(secret); + var result = new byte[decryptedSize]; Assert.Throws( - () => sut.Decrypt(secret), + () => sut.Decrypt(secret, result), ex => ex.Message.Contains("Decryption failed") ? null : "Expected Decryption failed in message"); } @@ -89,7 +96,8 @@ public class DekEncryptionServiceTests // act DekEncryptionService sut = new(secretProvider); - byte[] result = sut.Decrypt(secret); + byte[] result = new byte[sut.GetDecryptedSize(secret)]; + sut.Decrypt(secret, result); // verify Assert.Equal("Hello, World!"u8, result); diff --git a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionTests.cs index f040b84..32e4538 100644 --- a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs +++ b/IdentityShroud.Core.Tests/Services/EncryptionTests.cs @@ -19,7 +19,8 @@ public class EncryptionTests byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw="); // act - byte[] result = Encryption.Decrypt(cipher, keyValue); + byte[] result = new byte[Encryption.GetDecryptedLength(cipher)]; + Encryption.Decrypt(cipher, keyValue, result); // verify Assert.Equal("Hello, World!"u8, result); diff --git a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs b/IdentityShroud.Core/Contracts/IDekEncryptionService.cs index 3032040..bbb234c 100644 --- a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs +++ b/IdentityShroud.Core/Contracts/IDekEncryptionService.cs @@ -7,5 +7,7 @@ namespace IdentityShroud.Core.Contracts; public interface IDekEncryptionService { EncryptedDek Encrypt(ReadOnlySpan plain); - byte[] Decrypt(EncryptedDek input); + + void Decrypt(EncryptedDek input, Span output); + int GetDecryptedSize(EncryptedDek input); } \ No newline at end of file diff --git a/IdentityShroud.Core/CoreServiceCollectionExtensions.cs b/IdentityShroud.Core/CoreServiceCollectionExtensions.cs new file mode 100644 index 0000000..86d7339 --- /dev/null +++ b/IdentityShroud.Core/CoreServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.EFCore; +using IdentityShroud.Core.Security; +using IdentityShroud.Core.Security.Keys; +using IdentityShroud.Core.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace IdentityShroud.Core; + +public static class CoreServiceCollectionExtensions +{ + public static IServiceCollection AddCore(this IServiceCollection services) + { + services.AddScoped(); + + services.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithSingletonLifetime()); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + + + services.AddScoped(); + services.AddScoped(); + + + return services; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj b/IdentityShroud.Core/IdentityShroud.Core.csproj index fb54802..4562d8d 100644 --- a/IdentityShroud.Core/IdentityShroud.Core.csproj +++ b/IdentityShroud.Core/IdentityShroud.Core.csproj @@ -15,11 +15,16 @@ + + + + + diff --git a/IdentityShroud.Core/Model/DecryptedSigningKey.cs b/IdentityShroud.Core/Model/DecryptedSigningKey.cs new file mode 100644 index 0000000..4a94dc7 --- /dev/null +++ b/IdentityShroud.Core/Model/DecryptedSigningKey.cs @@ -0,0 +1,66 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Security.Keys; + +namespace IdentityShroud.Core.Model; + +public sealed class DecryptedSigningKey : IDisposable +{ + private readonly byte[] _keyData; + private readonly int _keyLength; + private bool _disposed; + + public RealmSigningKeyId Id { get; } + public KeyType KeyType { get; } + public ReadOnlySpan KeyData => _disposed + ? throw new ObjectDisposedException(nameof(DecryptedSigningKey)) + : _keyData.AsSpan(0, _keyLength); + + public DecryptedSigningKey(RealmSigningKey realmSigningKey, IDekEncryptionService encryptionService) + { + Id = realmSigningKey.Id; + KeyType = realmSigningKey.KeyType; + int keySize = encryptionService.GetDecryptedSize(realmSigningKey.Key); + _keyData = GC.AllocateArray(keySize, pinned: true); + _keyLength = keySize; + encryptionService.Decrypt(realmSigningKey.Key, _keyData); + } + + public DecryptedSigningKey() + { + Id = RealmSigningKeyId.NewId(); + KeyType = KeyType.RSA; + const int keySize = 2048; + + using var rsa = RSA.Create(); + rsa.KeySize = keySize; + int estimatedSize = EstimatePkcs8ExportSize(keySize); + + Span temp = stackalloc byte[estimatedSize * 2]; + try + { + if (!rsa.TryExportPkcs8PrivateKey(temp, out int bytesWritten)) + throw new CryptographicException("Unable to export RSA private key."); + + _keyData = GC.AllocateArray(bytesWritten, pinned: true); + _keyLength = bytesWritten; + temp[..bytesWritten].CopyTo(_keyData); + } + finally + { + CryptographicOperations.ZeroMemory(temp); + } + } + + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + CryptographicOperations.ZeroMemory(_keyData); + } + + // Note actual accurate coefficients would be *0.566 and +57.4 + public static int EstimatePkcs8ExportSize(int keySizeBits) + => ((keySizeBits * 6) / 10) + 150; +} \ No newline at end of file diff --git a/IdentityShroud.Core/Plugins/PluginLoader.cs b/IdentityShroud.Core/Plugins/PluginLoader.cs new file mode 100644 index 0000000..e216a57 --- /dev/null +++ b/IdentityShroud.Core/Plugins/PluginLoader.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Runtime.Loader; +using IdentityShroud.PluginSupport; + +namespace IdentityShroud.Core.Plugins; + +public static class PluginLoader +{ + public static IEnumerable LoadPlugins(string pluginsFolder) + { + if (!Directory.Exists(pluginsFolder)) + yield break; + + foreach (var dll in Directory.EnumerateFiles(pluginsFolder, "*.dll")) + { + foreach (var plugin in LoadPluginDll(dll)) yield return plugin; + } + } + + private static IEnumerable LoadPluginDll(string dll) + { + Assembly asm; + try + { + asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(dll)); + } + catch + { + yield break; + } + + IEnumerable pluginTypes; + try + { + pluginTypes = asm.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) && t is { IsInterface: false, IsAbstract: false }); + } + catch + { + yield break; + } + + foreach (var t in pluginTypes) + { + IPlugin? instance = null; + try + { + instance = (IPlugin?)Activator.CreateInstance(t); + } + catch + { + // ignore bad plugin types + } + + if (instance != null) + yield return instance; + } + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Plugins/PluginRegistry.cs b/IdentityShroud.Core/Plugins/PluginRegistry.cs new file mode 100644 index 0000000..d58863c --- /dev/null +++ b/IdentityShroud.Core/Plugins/PluginRegistry.cs @@ -0,0 +1,18 @@ +using System.Collections.ObjectModel; +using IdentityShroud.PluginSupport; + +namespace IdentityShroud.Core.Plugins; + +/// +/// Note +/// +/// +public class PluginRegistry where TPlugin : IPlugin +{ + private ReadOnlyDictionary _plugins; + + public PluginRegistry(ReadOnlyDictionary plugins) + { + _plugins = plugins; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Encryption.cs b/IdentityShroud.Core/Security/Encryption.cs index 5fe274e..01c8843 100644 --- a/IdentityShroud.Core/Security/Encryption.cs +++ b/IdentityShroud.Core/Security/Encryption.cs @@ -35,36 +35,45 @@ public static class Encryption return result; } - public static byte[] Decrypt(ReadOnlyMemory input, ReadOnlySpan key) + public static void Decrypt(ReadOnlyMemory input, ReadOnlySpan key, Span output) { - 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)); + AlgVersion versionParams = GetVersionParams(input); + if (input.Length < 1 + versionParams.NonceSize + versionParams.TagSize) + throw new ArgumentException("Cypher data is too short to be valid.", nameof(input)); + var payload = input.Span; 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); + aes.Decrypt(nonce, cipher, tag, output); } catch (CryptographicException ex) { // Tag verification failed → tampering or wrong key/nonce. throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex); } + } - return plaintext; + public static int GetDecryptedLength(ReadOnlyMemory input) + { + AlgVersion versionParams = GetVersionParams(input); + int length = input.Length - (1 + versionParams.NonceSize + versionParams.TagSize); + if (length < 0) + throw new ArgumentException("Cypher data is too short to be valid.", nameof(input)); + + return length; + } + + private static AlgVersion GetVersionParams(ReadOnlyMemory input) + { + var versionNumber = (int)input.Span[0]; + if (versionNumber != 1) + throw new ArgumentException("Invalid payload"); + + return _versions[versionNumber]; } } \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/IJwtSignatureProvider.cs b/IdentityShroud.Core/Security/Jwt/IJwtSigner.cs similarity index 61% rename from IdentityShroud.Core/Security/Jwt/IJwtSignatureProvider.cs rename to IdentityShroud.Core/Security/Jwt/IJwtSigner.cs index 5100436..80fc37e 100644 --- a/IdentityShroud.Core/Security/Jwt/IJwtSignatureProvider.cs +++ b/IdentityShroud.Core/Security/Jwt/IJwtSigner.cs @@ -1,8 +1,9 @@ using System.Text.Json; +using IdentityShroud.Core.Model; namespace IdentityShroud.Core; -public interface IJwtSignatureProvider : IDisposable +public interface IJwtSigner { /* Of the signature and MAC algorithms specified in JSON Web Algorithms @@ -13,14 +14,7 @@ public interface IJwtSignatureProvider : IDisposable hash algorithm ("ES256"). Support for other algorithms and key sizes is OPTIONAL. */ + IReadOnlyList Algorithms { get; } - void WriteJwtHeaderFields(Utf8JsonWriter writer); - /// - /// Length of the binary signature in bytes. - /// - /// - int GetSignatureLength(); - - void CalculateSignature(ReadOnlySpan jwt, Span signatureOut); - + byte[] CalculateSignature(JwtSigAlgName algName, DecryptedSigningKey key, ReadOnlySpan jwt); } \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs b/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs new file mode 100644 index 0000000..fbab369 --- /dev/null +++ b/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs @@ -0,0 +1,6 @@ +namespace IdentityShroud.Core; + +public interface IJwtSignerFactory +{ + IJwtSigner Create(JwtSigAlgName algorithm); +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs b/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs index 6143f35..99b9097 100644 --- a/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs +++ b/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs @@ -2,6 +2,7 @@ using System.Buffers.Text; using System.Security.Cryptography; using System.Text; using System.Text.Json; +using IdentityShroud.Core.Model; using Microsoft.AspNetCore.WebUtilities; namespace IdentityShroud.Core; @@ -40,49 +41,48 @@ public static class JwtSignatureGenerator } -public static class JwtCreator +public class JwtService(IJwtSignerFactory signerFactory) { - public static byte[] CreateEncodedJwt(ReadOnlySpan payloadUtf8, IJwtSignatureProvider signatureProvider) + public byte[] CreateEncodedJwt(ReadOnlySpan payloadUtf8, JwtSigAlgName algName, DecryptedSigningKey key) { - MemoryStream memStream = new(); - Utf8JsonWriter writer = new(memStream); - WriteJwtHeader(writer, signatureProvider); - writer.Flush(); - memStream.Seek(0, SeekOrigin.Begin); + // LATER might be able to improve performance using ArrayPool + + IJwtSigner signer = signerFactory.Create(algName); + MemoryStream headerMemStream = new(); + Utf8JsonWriter headerWriter = new(headerMemStream); + WriteJwtHeader(headerWriter, algName, key.Id.ToString()); + headerWriter.Flush(); + headerMemStream.Seek(0, SeekOrigin.Begin); - int headerBase64Length = Base64Url.GetEncodedLength((int)memStream.Length); + int headerBase64Length = Base64Url.GetEncodedLength((int)headerMemStream.Length); int payloadBase64Length = Base64Url.GetEncodedLength(payloadUtf8.Length); - int signatureBase64Length = Base64Url.GetEncodedLength(signatureProvider.GetSignatureLength()); - int totalLength = headerBase64Length + 1 + payloadBase64Length + 1 + signatureBase64Length; - - var completeJwt = new byte[totalLength]; + var jwtData = new byte[headerBase64Length + payloadBase64Length + 1]; // - var byteArray = new byte[memStream.Length]; - memStream.ReadExactly(byteArray, 0, (int)memStream.Length); - int written = Base64Url.EncodeToUtf8(byteArray, completeJwt); + var byteArray = new byte[headerMemStream.Length]; + headerMemStream.ReadExactly(byteArray, 0, (int)headerMemStream.Length); + int written = Base64Url.EncodeToUtf8(byteArray, jwtData); if (written != headerBase64Length) throw new Exception("expected header length did not match bytes written"); - completeJwt[headerBase64Length] = (byte)'.'; + jwtData[headerBase64Length] = (byte)'.'; - written = Base64Url.EncodeToUtf8(payloadUtf8, completeJwt.AsSpan().Slice(headerBase64Length + 1, payloadBase64Length)); + written = Base64Url.EncodeToUtf8(payloadUtf8, jwtData.AsSpan().Slice(headerBase64Length + 1, payloadBase64Length)); if (written != payloadBase64Length) throw new Exception("expected payload length did not match bytes written"); - completeJwt[headerBase64Length + 1 + payloadBase64Length] = (byte)'.'; + byte[] signature = signer.CalculateSignature(algName, key, jwtData.AsSpan()); + int signatureBase64Length = Base64Url.GetEncodedLength(signature.Length); - Span signature = stackalloc byte[signatureProvider.GetSignatureLength()]; - signatureProvider.CalculateSignature( - completeJwt.AsSpan().Slice(0, headerBase64Length + 1 + payloadBase64Length), - signature); - - written = Base64Url.EncodeToUtf8(signature, completeJwt.AsSpan() - .Slice(headerBase64Length + 1 + payloadBase64Length + 1)); + var completeJwt = new byte[jwtData.Length + 1 + signatureBase64Length]; + Array.Copy(jwtData, completeJwt, jwtData.Length); + completeJwt[jwtData.Length] = (byte)'.'; + + written = Base64Url.EncodeToUtf8(signature, completeJwt.AsSpan().Slice(jwtData.Length + 1, signatureBase64Length)); if (written != signatureBase64Length) throw new Exception("expected signature length did not match bytes written"); @@ -90,11 +90,12 @@ public static class JwtCreator return completeJwt; } - private static void WriteJwtHeader(Utf8JsonWriter writer, IJwtSignatureProvider signatureProvider) + private static void WriteJwtHeader(Utf8JsonWriter writer, JwtSigAlgName algName, string keyId) { writer.WriteStartObject(); writer.WriteString("typ"u8, "JWT"u8); - signatureProvider.WriteJwtHeaderFields(writer); + writer.WriteString("alg"u8, algName.ToString()); + writer.WriteString("kid"u8, keyId); writer.WriteEndObject(); } } \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs b/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs new file mode 100644 index 0000000..85ffe21 --- /dev/null +++ b/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs @@ -0,0 +1,17 @@ +namespace IdentityShroud.Core; + +public class JwtSignerFactory(IEnumerable signers) : IJwtSignerFactory +{ + private readonly IReadOnlyDictionary _signers = signers + .SelectMany(s => s.Algorithms.Select(alg => (alg, signer: s))) + .ToDictionary(x => x.alg, x => x.signer); + + public IJwtSigner Create(JwtSigAlgName algorithm) + { + if (_signers.TryGetValue(algorithm, out var signer)) + return signer; + + throw new NotSupportedException($"JWT signing algorithm '{algorithm}' is not registered."); + } + +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs b/IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs deleted file mode 100644 index ab33a72..0000000 --- a/IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Security.Cryptography; -using System.Text.Json; -using IdentityShroud.Core.Model; -using IdentityShroud.Core.Services; - -namespace IdentityShroud.Core; - -public class SignatureProviderFactory(DekEncryptionService dekCryptor, IServiceProvider services) -{ - - public static void SelectAlgorithmAndKey(Realm realm, Client client, out JwtSigAlgName alg, out byte[] key) - { - throw new NotImplementedException(); - } - - public IJwtSignatureProvider Create(JwtSigAlgName algorithm, byte[] keyData) - { - - - //realm.DefaultSignatureAlgorithm - //realm.TokenSigningKeys - - //IJwtSignatureProvider? sigProvider = services.GetKeyedService(); - throw new NotImplementedException(); - } - -} - -public class RsaJwtSignatureProvider : IJwtSignatureProvider -{ - private JwtSigAlgName _sigAlgName; - private RealmSigningKeyId _keyId; - private readonly RSA _rsa; - - public RsaJwtSignatureProvider(DekEncryptionService dekCryptor, - RealmSigningKey privateKey, - JwtSigAlgName sigAlgName) - { - _sigAlgName = sigAlgName; - _keyId = privateKey.Id; - - byte[] key = dekCryptor.Decrypt(privateKey.Key); - _rsa = RSA.Create(); - _rsa.ImportPkcs8PrivateKey(key, out int _); - } - - /* - +-------------------+---------------------------------+ - | "alg" Param Value | Digital Signature Algorithm | - +-------------------+---------------------------------+ - | RS256 | RSASSA-PKCS1-v1_5 using SHA-256 | - | RS384 | RSASSA-PKCS1-v1_5 using SHA-384 | - | RS512 | RSASSA-PKCS1-v1_5 using SHA-512 | - +-------------------+---------------------------------+ - */ - - - public void WriteJwtHeaderFields(Utf8JsonWriter writer) - { - writer.WriteString("alg"u8, _sigAlgName.ToString()); - writer.WriteString("kid"u8, _keyId.ToString()); - } - - public int GetSignatureLength() - { - return _rsa.KeySize / 8; - } - - public void CalculateSignature(ReadOnlySpan jwt, Span sig) - { - _rsa.SignData(jwt, sig, GetHashAlgorithmName(), RSASignaturePadding.Pkcs1); - } - - public void Dispose() - { - _rsa.Dispose(); - } - - private HashAlgorithmName GetHashAlgorithmName() - => _sigAlgName.Name switch - { - "RS256" => HashAlgorithmName.SHA256, - "RS384" => HashAlgorithmName.SHA384, - "RS512" => HashAlgorithmName.SHA512, - _ => throw new ArgumentException("Invalid algorithm for RsaJwtSignatureProvider") - }; - -} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs b/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs new file mode 100644 index 0000000..80af03c --- /dev/null +++ b/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs @@ -0,0 +1,36 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Model; + +namespace IdentityShroud.Core; + +public class RsaJwtSigner : IJwtSigner +{ + public IReadOnlyList Algorithms => [JwtSigAlgName.RS256, JwtSigAlgName.RS384, JwtSigAlgName.RS512]; + + // +-------------------+---------------------------------+ + // | "alg" Param Value | Digital Signature Algorithm | + // +-------------------+---------------------------------+ + // | RS256 | RSASSA-PKCS1-v1_5 using SHA-256 | + // | RS384 | RSASSA-PKCS1-v1_5 using SHA-384 | + // | RS512 | RSASSA-PKCS1-v1_5 using SHA-512 | + // +-------------------+---------------------------------+ + + public byte[] CalculateSignature(JwtSigAlgName algName, DecryptedSigningKey key, ReadOnlySpan jwt) + { + using var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(key.KeyData, out int _); + var sig = new byte[rsa.KeySize / 8]; + rsa.SignData(jwt, sig, GetHashAlgorithmName(algName), RSASignaturePadding.Pkcs1); + return sig; + } + + private static HashAlgorithmName GetHashAlgorithmName(JwtSigAlgName algName) + => algName.Name switch + { + "RS256" => HashAlgorithmName.SHA256, + "RS384" => HashAlgorithmName.SHA384, + "RS512" => HashAlgorithmName.SHA512, + _ => throw new ArgumentException("Invalid algorithm for RsaJwtSignatureProvider") + }; + +} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/DataEncryptionService.cs b/IdentityShroud.Core/Services/DataEncryptionService.cs index 9d8f092..be0cf51 100644 --- a/IdentityShroud.Core/Services/DataEncryptionService.cs +++ b/IdentityShroud.Core/Services/DataEncryptionService.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Model; using IdentityShroud.Core.Security; @@ -9,9 +10,17 @@ public class DataEncryptionService( { public EncryptedValue Encrypt(RealmDek dek, ReadOnlySpan plain) { - var key = dekCryptor.Decrypt(dek.KeyData); - byte[] cipher = Encryption.Encrypt(plain, key); - return new (dek.Id, cipher); + Span key = stackalloc byte[dekCryptor.GetDecryptedSize(dek.KeyData)]; + try + { + dekCryptor.Decrypt(dek.KeyData, key); + byte[] cipher = Encryption.Encrypt(plain, key); + return new (dek.Id, cipher); + } + finally + { + CryptographicOperations.ZeroMemory(key); + } } public byte[] Decrypt(IReadOnlyList deks, EncryptedValue input) @@ -20,8 +29,19 @@ public class DataEncryptionService( // - Old key removed before migration completed (should not be possible) // - Wrong keyset because of programming error. var dek = deks.SingleOrDefault(d => d.Id == input.DekId) - ?? throw new InvalidOperationException("Required key not found"); - var key = dekCryptor.Decrypt(dek.KeyData); - return Encryption.Decrypt(input.Value, key); + ?? throw new InvalidOperationException("Required key not found"); + + Span key = stackalloc byte[dekCryptor.GetDecryptedSize(dek.KeyData)]; + try + { + dekCryptor.Decrypt(dek.KeyData, key); + byte[] output = new byte[Encryption.GetDecryptedLength(input.Value)]; + Encryption.Decrypt(input.Value, key, output); + return output; + } + finally + { + CryptographicOperations.ZeroMemory(key); + } } } \ No newline at end of file diff --git a/IdentityShroud.Core/Services/DekEncryptionService.cs b/IdentityShroud.Core/Services/DekEncryptionService.cs index add9267..b80ea4d 100644 --- a/IdentityShroud.Core/Services/DekEncryptionService.cs +++ b/IdentityShroud.Core/Services/DekEncryptionService.cs @@ -18,8 +18,6 @@ public class DekEncryptionService : IDekEncryptionService 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) @@ -29,10 +27,14 @@ public class DekEncryptionService : IDekEncryptionService return new (encryptionKey.Id, cipher); } - public byte[] Decrypt(EncryptedDek input) + public void Decrypt(EncryptedDek input, Span output) { var encryptionKey = GetKey(input.KekId); + Encryption.Decrypt(input.Value, encryptionKey.Key, output); + } - return Encryption.Decrypt(input.Value, encryptionKey.Key); + public int GetDecryptedSize(EncryptedDek input) + { + return Encryption.GetDecryptedLength(input.Value); } } \ No newline at end of file diff --git a/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj b/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj new file mode 100644 index 0000000..b0de15c --- /dev/null +++ b/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/IdentityShroud.GraphQL/Query.cs b/IdentityShroud.GraphQL/Query.cs new file mode 100644 index 0000000..5ccbeb1 --- /dev/null +++ b/IdentityShroud.GraphQL/Query.cs @@ -0,0 +1,26 @@ +susing IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Model; + +namespace IdentityShroud.GraphQL; + +public class Query +{ + public string GetHello() => "Hello, world!"; + + public async Task GetRealms( + Guid id, + [Service] IRealmService realmService) + { + return await realmService.FindById(id); + } +} + +public class Mutation +{ + public async Task RealmCreate(string name) + { + Realm r = new(); + + return r; + } +} \ No newline at end of file diff --git a/IdentityShroud.GraphQL/RegistrationExtensions.cs b/IdentityShroud.GraphQL/RegistrationExtensions.cs new file mode 100644 index 0000000..dad3056 --- /dev/null +++ b/IdentityShroud.GraphQL/RegistrationExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace IdentityShroud.GraphQL; + +public static class RegistrationExtensions +{ + extension(IServiceCollection services) + { + public IServiceCollection AddIdentityShroudGraphQL() + { + services + .AddGraphQLServer() + .AddMutationConventions(applyToAllMutations: true) + .AddMutationType() + .AddQueryType(); + + return services; + } + } + + extension(IEndpointRouteBuilder app) + { + public IEndpointRouteBuilder MapIdentityShroudGraphQL() + { + app.MapGraphQL(); + return app; + } + } +} \ No newline at end of file diff --git a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs new file mode 100644 index 0000000..6c3df6d --- /dev/null +++ b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs @@ -0,0 +1,318 @@ +// +using System; +using System.Collections.Generic; +using IdentityShroud.Core.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IdentityShroud.Migrations.Migrations +{ + [DbContext(typeof(Db))] + [Migration("20260412083710_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowClientCredentialsFlow") + .HasColumnType("boolean") + .HasColumnName("allow_client_credentials_flow"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("client_id"); + + b.Property("Confidential") + .HasColumnType("boolean") + .HasColumnName("confidential"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description"); + + b.Property("Name") + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("name"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("SignatureAlgorithm") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("signature_algorithm"); + + b.HasKey("Id") + .HasName("pk_client"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_client_client_id"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_client_realm_id"); + + b.ToTable("client", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("uuid") + .HasColumnName("client_id"); + + b.Property("ClientId1") + .HasColumnType("integer") + .HasColumnName("client_id1"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.ComplexProperty(typeof(Dictionary), "Secret", "IdentityShroud.Core.Model.ClientSecret.Secret#EncryptedValue", b1 => + { + b1.IsRequired(); + + b1.Property("DekId") + .HasColumnType("uuid") + .HasColumnName("secret_dek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("secret_value"); + }); + + b.HasKey("Id") + .HasName("pk_client_secret"); + + b.HasIndex("ClientId1") + .HasDatabaseName("ix_client_secret_client_id1"); + + b.ToTable("client_secret", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DefaultSignatureAlgorithm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("default_signature_algorithm"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_realm"); + + b.ToTable("realm", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Algorithm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("algorithm"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.ComplexProperty(typeof(Dictionary), "KeyData", "IdentityShroud.Core.Model.RealmDek.KeyData#EncryptedDek", b1 => + { + b1.IsRequired(); + + b1.Property("KekId") + .HasColumnType("uuid") + .HasColumnName("key_data_kek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("key_data_value"); + }); + + b.HasKey("Id") + .HasName("pk_realm_dek"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_realm_dek_realm_id"); + + b.ToTable("realm_dek", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("KeyType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key_type"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("PublicKeyParameters") + .HasColumnType("jsonb") + .HasColumnName("public_key_parameters"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.ComplexProperty(typeof(Dictionary), "Key", "IdentityShroud.Core.Model.RealmSigningKey.Key#EncryptedDek", b1 => + { + b1.IsRequired(); + + b1.Property("KekId") + .HasColumnType("uuid") + .HasColumnName("key_kek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("key_value"); + }); + + b.HasKey("Id") + .HasName("pk_realm_key"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_realm_key_realm_id"); + + b.ToTable("realm_key", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("Clients") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => + { + b.HasOne("IdentityShroud.Core.Model.Client", null) + .WithMany("Secrets") + .HasForeignKey("ClientId1") + .HasConstraintName("fk_client_secret_client_client_id1"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("DataEncryptionKeys") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_dek_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("TokenSigningKeys") + .HasForeignKey("RealmId") + .HasConstraintName("fk_realm_key_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.Navigation("Secrets"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => + { + b.Navigation("Clients"); + + b.Navigation("DataEncryptionKeys"); + + b.Navigation("TokenSigningKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs new file mode 100644 index 0000000..78401bb --- /dev/null +++ b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs @@ -0,0 +1,171 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IdentityShroud.Migrations.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "realm", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + slug = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + default_signature_algorithm = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_realm", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "client", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + realm_id = table.Column(type: "uuid", nullable: false), + client_id = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + name = table.Column(type: "character varying(80)", maxLength: 80, nullable: true), + description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + signature_algorithm = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + confidential = table.Column(type: "boolean", nullable: false), + allow_client_credentials_flow = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_client", x => x.id); + table.ForeignKey( + name: "fk_client_realm_realm_id", + column: x => x.realm_id, + principalTable: "realm", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "realm_dek", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + active = table.Column(type: "boolean", nullable: false), + algorithm = table.Column(type: "text", nullable: false), + realm_id = table.Column(type: "uuid", nullable: false), + key_data_kek_id = table.Column(type: "uuid", nullable: false), + key_data_value = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_realm_dek", x => x.id); + table.ForeignKey( + name: "fk_realm_dek_realm_realm_id", + column: x => x.realm_id, + principalTable: "realm", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "realm_key", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + key_type = table.Column(type: "text", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + revoked_at = table.Column(type: "timestamp with time zone", nullable: true), + priority = table.Column(type: "integer", nullable: false), + public_key_parameters = table.Column(type: "jsonb", nullable: true), + realm_id = table.Column(type: "uuid", nullable: true), + key_kek_id = table.Column(type: "uuid", nullable: false), + key_value = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_realm_key", x => x.id); + table.ForeignKey( + name: "fk_realm_key_realm_realm_id", + column: x => x.realm_id, + principalTable: "realm", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "client_secret", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + client_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + expires = table.Column(type: "timestamp with time zone", nullable: true), + revoked_at = table.Column(type: "timestamp with time zone", nullable: true), + client_id1 = table.Column(type: "integer", nullable: true), + secret_dek_id = table.Column(type: "uuid", nullable: false), + secret_value = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_client_secret", x => x.id); + table.ForeignKey( + name: "fk_client_secret_client_client_id1", + column: x => x.client_id1, + principalTable: "client", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_client_client_id", + table: "client", + column: "client_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_client_realm_id", + table: "client", + column: "realm_id"); + + migrationBuilder.CreateIndex( + name: "ix_client_secret_client_id1", + table: "client_secret", + column: "client_id1"); + + migrationBuilder.CreateIndex( + name: "ix_realm_dek_realm_id", + table: "realm_dek", + column: "realm_id"); + + migrationBuilder.CreateIndex( + name: "ix_realm_key_realm_id", + table: "realm_key", + column: "realm_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "client_secret"); + + migrationBuilder.DropTable( + name: "realm_dek"); + + migrationBuilder.DropTable( + name: "realm_key"); + + migrationBuilder.DropTable( + name: "client"); + + migrationBuilder.DropTable( + name: "realm"); + } + } +} diff --git a/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs b/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs new file mode 100644 index 0000000..f16ec76 --- /dev/null +++ b/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs @@ -0,0 +1,315 @@ +// +using System; +using System.Collections.Generic; +using IdentityShroud.Core.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IdentityShroud.Migrations.Migrations +{ + [DbContext(typeof(Db))] + partial class DbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowClientCredentialsFlow") + .HasColumnType("boolean") + .HasColumnName("allow_client_credentials_flow"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("client_id"); + + b.Property("Confidential") + .HasColumnType("boolean") + .HasColumnName("confidential"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description"); + + b.Property("Name") + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasColumnName("name"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("SignatureAlgorithm") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("signature_algorithm"); + + b.HasKey("Id") + .HasName("pk_client"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_client_client_id"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_client_realm_id"); + + b.ToTable("client", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("uuid") + .HasColumnName("client_id"); + + b.Property("ClientId1") + .HasColumnType("integer") + .HasColumnName("client_id1"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.ComplexProperty(typeof(Dictionary), "Secret", "IdentityShroud.Core.Model.ClientSecret.Secret#EncryptedValue", b1 => + { + b1.IsRequired(); + + b1.Property("DekId") + .HasColumnType("uuid") + .HasColumnName("secret_dek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("secret_value"); + }); + + b.HasKey("Id") + .HasName("pk_client_secret"); + + b.HasIndex("ClientId1") + .HasDatabaseName("ix_client_secret_client_id1"); + + b.ToTable("client_secret", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DefaultSignatureAlgorithm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("default_signature_algorithm"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_realm"); + + b.ToTable("realm", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Algorithm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("algorithm"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.ComplexProperty(typeof(Dictionary), "KeyData", "IdentityShroud.Core.Model.RealmDek.KeyData#EncryptedDek", b1 => + { + b1.IsRequired(); + + b1.Property("KekId") + .HasColumnType("uuid") + .HasColumnName("key_data_kek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("key_data_value"); + }); + + b.HasKey("Id") + .HasName("pk_realm_dek"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_realm_dek_realm_id"); + + b.ToTable("realm_dek", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("KeyType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key_type"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("PublicKeyParameters") + .HasColumnType("jsonb") + .HasColumnName("public_key_parameters"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.ComplexProperty(typeof(Dictionary), "Key", "IdentityShroud.Core.Model.RealmSigningKey.Key#EncryptedDek", b1 => + { + b1.IsRequired(); + + b1.Property("KekId") + .HasColumnType("uuid") + .HasColumnName("key_kek_id"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("key_value"); + }); + + b.HasKey("Id") + .HasName("pk_realm_key"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_realm_key_realm_id"); + + b.ToTable("realm_key", (string)null); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("Clients") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => + { + b.HasOne("IdentityShroud.Core.Model.Client", null) + .WithMany("Secrets") + .HasForeignKey("ClientId1") + .HasConstraintName("fk_client_secret_client_client_id1"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("DataEncryptionKeys") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_dek_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => + { + b.HasOne("IdentityShroud.Core.Model.Realm", null) + .WithMany("TokenSigningKeys") + .HasForeignKey("RealmId") + .HasConstraintName("fk_realm_key_realm_realm_id"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => + { + b.Navigation("Secrets"); + }); + + modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => + { + b.Navigation("Clients"); + + b.Navigation("DataEncryptionKeys"); + + b.Navigation("TokenSigningKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/IdentityShroud.PluginSupport/IPlugin.cs b/IdentityShroud.PluginSupport/IPlugin.cs new file mode 100644 index 0000000..fcae4fc --- /dev/null +++ b/IdentityShroud.PluginSupport/IPlugin.cs @@ -0,0 +1,9 @@ +namespace IdentityShroud.PluginSupport; + +/// +/// Any class that should be discovered when loading a dll should implement IPlugin +/// +public interface IPlugin +{ + +} \ No newline at end of file diff --git a/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj b/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/IdentityShroud.SecretProviders/ISecretProvider.cs b/IdentityShroud.SecretProviders/ISecretProvider.cs new file mode 100644 index 0000000..666dd1e --- /dev/null +++ b/IdentityShroud.SecretProviders/ISecretProvider.cs @@ -0,0 +1,15 @@ +namespace IdentityShroud.SecretProviders; + +/// +/// Required interface of a SecretProvider. +/// +public interface ISecretProvider +{ + /// + /// Used as a key in the registry. This same value should be used for the type field + /// when configuring a secret. + /// + string Key { get; } + + Task GetSecret(string configurationValue, CancellationToken ct = default); +} \ No newline at end of file diff --git a/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj b/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj new file mode 100644 index 0000000..3de771e --- /dev/null +++ b/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/IdentityShroud.SecretProviders/PlainSecret.cs b/IdentityShroud.SecretProviders/PlainSecret.cs new file mode 100644 index 0000000..9aca02a --- /dev/null +++ b/IdentityShroud.SecretProviders/PlainSecret.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.SecretProviders; + +public sealed class PlainSecret(byte[] secret) : IDisposable +{ + private bool _disposed; + + public ReadOnlySpan Secret => secret; + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + CryptographicOperations.ZeroMemory(secret); + } +} \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs b/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs index 879f932..84f9cd7 100644 --- a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs +++ b/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs @@ -11,6 +11,16 @@ public class NullDekEncryptionService : IDekEncryptionService return new(KeyId, plain.ToArray()); } + public void Decrypt(EncryptedDek input, Span output) + { + input.Value.CopyTo(output); + } + + public int GetDecryptedSize(EncryptedDek input) + { + return input.Value.Length; + } + public byte[] Decrypt(EncryptedDek input) { return input.Value; diff --git a/IdentityShroud.sln b/IdentityShroud.sln index 4fd0005..ce316d1 100644 --- a/IdentityShroud.sln +++ b/IdentityShroud.sln @@ -20,6 +20,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "08_Tests", "08_Tests", "{98 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01", "01", "{07B08872-1141-4BE6-87E6-B85E52FE4341}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.GraphQL", "IdentityShroud.GraphQL\IdentityShroud.GraphQL.csproj", "{8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "04 Plugin Support", "04 Plugin Support", "{ABF0B435-2D50-41C8-847B-0905136537AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.SecretProviders", "IdentityShroud.SecretProviders\IdentityShroud.SecretProviders.csproj", "{A27A70F7-415B-4F06-8A2C-1399675D41AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.PluginSupport", "IdentityShroud.PluginSupport\IdentityShroud.PluginSupport.csproj", "{D4984187-8283-4494-88CF-35EDAD13E4DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +62,18 @@ Global {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Debug|Any CPU.Build.0 = Debug|Any CPU {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.ActiveCfg = Release|Any CPU {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.Build.0 = Release|Any CPU + {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Release|Any CPU.Build.0 = Release|Any CPU + {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Release|Any CPU.Build.0 = Release|Any CPU + {D4984187-8283-4494-88CF-35EDAD13E4DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4984187-8283-4494-88CF-35EDAD13E4DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4984187-8283-4494-88CF-35EDAD13E4DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4984187-8283-4494-88CF-35EDAD13E4DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4758FE2E-A437-44F0-B58E-09E52D67D288} = {980900AA-E052-498B-A41A-4F33A8678828} @@ -62,5 +82,8 @@ Global {A8554BCC-C9B6-4D96-90AD-FE80E95441F4} = {980900AA-E052-498B-A41A-4F33A8678828} {D2B446A0-AB62-4555-9D79-33FF43D7CEF4} = {07B08872-1141-4BE6-87E6-B85E52FE4341} {8490BF59-B68A-4BE0-9F96-6CB262AF4850} = {07B08872-1141-4BE6-87E6-B85E52FE4341} + {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8} = {07B08872-1141-4BE6-87E6-B85E52FE4341} + {A27A70F7-415B-4F06-8A2C-1399675D41AE} = {ABF0B435-2D50-41C8-847B-0905136537AB} + {D4984187-8283-4494-88CF-35EDAD13E4DE} = {ABF0B435-2D50-41C8-847B-0905136537AB} EndGlobalSection EndGlobal diff --git a/IdentityShroud.sln.DotSettings.user b/IdentityShroud.sln.DotSettings.user index 158c9b3..0e215b5 100644 --- a/IdentityShroud.sln.DotSettings.user +++ b/IdentityShroud.sln.DotSettings.user @@ -25,6 +25,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -54,8 +55,13 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded - /home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr + + + + +