Reworked encryption to use less heap allocated buffers for secrets.
Also some work on plugin system.
This commit is contained in:
parent
8782ef39c6
commit
054754f553
42 changed files with 1452 additions and 242 deletions
|
|
@ -7,5 +7,7 @@ namespace IdentityShroud.Core.Contracts;
|
|||
public interface IDekEncryptionService
|
||||
{
|
||||
EncryptedDek Encrypt(ReadOnlySpan<byte> plain);
|
||||
byte[] Decrypt(EncryptedDek input);
|
||||
|
||||
void Decrypt(EncryptedDek input, Span<byte> output);
|
||||
int GetDecryptedSize(EncryptedDek input);
|
||||
}
|
||||
38
IdentityShroud.Core/CoreServiceCollectionExtensions.cs
Normal file
38
IdentityShroud.Core/CoreServiceCollectionExtensions.cs
Normal file
|
|
@ -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<Db>();
|
||||
|
||||
services.Scan(scan => scan
|
||||
.FromAssemblyOf<RealmService>()
|
||||
.AddClasses(classes => classes.AssignableTo<IJwtSigner>())
|
||||
.AsImplementedInterfaces()
|
||||
.WithSingletonLifetime());
|
||||
services.AddSingleton<IJwtSignerFactory, JwtSignerFactory>();
|
||||
|
||||
services.AddSingleton<IClock, ClockService>();
|
||||
services.AddSingleton<IDekEncryptionService, DekEncryptionService>();
|
||||
services.AddScoped<IDataEncryptionService, DataEncryptionService>();
|
||||
services.AddScoped<IRealmContext, RealmContext>();
|
||||
services.AddScoped<IKeyProviderFactory, KeyProviderFactory>();
|
||||
services.AddScoped<IKeyService, KeyService>();
|
||||
services.AddSingleton<ISecretProvider, ConfigurationSecretProvider>();
|
||||
|
||||
|
||||
services.AddScoped<IClientService, ClientService>();
|
||||
services.AddScoped<IRealmService, RealmService>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,16 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Scrutor" Version="7.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="FluentResults" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IdentityShroud.PluginSupport\IdentityShroud.PluginSupport.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
66
IdentityShroud.Core/Model/DecryptedSigningKey.cs
Normal file
66
IdentityShroud.Core/Model/DecryptedSigningKey.cs
Normal file
|
|
@ -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<byte> 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<byte>(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<byte> 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<byte>(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;
|
||||
}
|
||||
59
IdentityShroud.Core/Plugins/PluginLoader.cs
Normal file
59
IdentityShroud.Core/Plugins/PluginLoader.cs
Normal file
|
|
@ -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<IPlugin> 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<IPlugin> LoadPluginDll(string dll)
|
||||
{
|
||||
Assembly asm;
|
||||
try
|
||||
{
|
||||
asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(dll));
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
IEnumerable<Type> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
IdentityShroud.Core/Plugins/PluginRegistry.cs
Normal file
18
IdentityShroud.Core/Plugins/PluginRegistry.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using IdentityShroud.PluginSupport;
|
||||
|
||||
namespace IdentityShroud.Core.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Note
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugin"></typeparam>
|
||||
public class PluginRegistry<TPlugin> where TPlugin : IPlugin
|
||||
{
|
||||
private ReadOnlyDictionary<string, TPlugin> _plugins;
|
||||
|
||||
public PluginRegistry(ReadOnlyDictionary<string, TPlugin> plugins)
|
||||
{
|
||||
_plugins = plugins;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,36 +35,45 @@ public static class Encryption
|
|||
return result;
|
||||
}
|
||||
|
||||
public static byte[] Decrypt(ReadOnlyMemory<byte> input, ReadOnlySpan<byte> key)
|
||||
public static void Decrypt(ReadOnlyMemory<byte> input, ReadOnlySpan<byte> key, Span<byte> 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<byte> nonce = payload.Slice(1, versionParams.NonceSize);
|
||||
ReadOnlySpan<byte> tag = payload.Slice(1 + versionParams.NonceSize, versionParams.TagSize);
|
||||
ReadOnlySpan<byte> 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<byte> 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<byte> input)
|
||||
{
|
||||
var versionNumber = (int)input.Span[0];
|
||||
if (versionNumber != 1)
|
||||
throw new ArgumentException("Invalid payload");
|
||||
|
||||
return _versions[versionNumber];
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JwtSigAlgName> Algorithms { get; }
|
||||
|
||||
void WriteJwtHeaderFields(Utf8JsonWriter writer);
|
||||
/// <summary>
|
||||
/// Length of the binary signature in bytes.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
int GetSignatureLength();
|
||||
|
||||
void CalculateSignature(ReadOnlySpan<byte> jwt, Span<byte> signatureOut);
|
||||
|
||||
byte[] CalculateSignature(JwtSigAlgName algName, DecryptedSigningKey key, ReadOnlySpan<byte> jwt);
|
||||
}
|
||||
6
IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs
Normal file
6
IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace IdentityShroud.Core;
|
||||
|
||||
public interface IJwtSignerFactory
|
||||
{
|
||||
IJwtSigner Create(JwtSigAlgName algorithm);
|
||||
}
|
||||
|
|
@ -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<byte> payloadUtf8, IJwtSignatureProvider signatureProvider)
|
||||
public byte[] CreateEncodedJwt(ReadOnlySpan<byte> 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<byte> 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();
|
||||
}
|
||||
}
|
||||
17
IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs
Normal file
17
IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace IdentityShroud.Core;
|
||||
|
||||
public class JwtSignerFactory(IEnumerable<IJwtSigner> signers) : IJwtSignerFactory
|
||||
{
|
||||
private readonly IReadOnlyDictionary<JwtSigAlgName, IJwtSigner> _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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<IJwtSignatureProvider>();
|
||||
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<byte> jwt, Span<byte> 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")
|
||||
};
|
||||
|
||||
}
|
||||
36
IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs
Normal file
36
IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Model;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public class RsaJwtSigner : IJwtSigner
|
||||
{
|
||||
public IReadOnlyList<JwtSigAlgName> 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<byte> 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")
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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<byte> plain)
|
||||
{
|
||||
var key = dekCryptor.Decrypt(dek.KeyData);
|
||||
byte[] cipher = Encryption.Encrypt(plain, key);
|
||||
return new (dek.Id, cipher);
|
||||
Span<byte> 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<RealmDek> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue