Reworked encryption to use less heap allocated buffers for secrets.

Also some work on plugin system.
This commit is contained in:
eelke 2026-08-18 07:40:24 +02:00
parent 8782ef39c6
commit 054754f553
42 changed files with 1452 additions and 242 deletions

View file

@ -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];
}
}

View file

@ -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);
}

View file

@ -0,0 +1,6 @@
namespace IdentityShroud.Core;
public interface IJwtSignerFactory
{
IJwtSigner Create(JwtSigAlgName algorithm);
}

View file

@ -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();
}
}

View 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.");
}
}

View file

@ -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")
};
}

View 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")
};
}