using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; using FinalFactory.Rendezvous.Server.State; namespace FinalFactory.Rendezvous.Server.Sessions; internal interface ISessionCapabilityService { string CreateDerivationSalt(); string DeriveCapability( string purpose, string ownerSubject, string idempotencyKey, string requestFingerprint, string derivationSalt); Guid DeriveGuid( string purpose, string ownerSubject, string idempotencyKey, string requestFingerprint, string derivationSalt); string DeriveOpaqueIdentifier(string purpose, string value); bool TryFingerprint(string? capability, out SecretFingerprint fingerprint); } internal sealed class EphemeralCapabilityIssuer : ISessionCapabilityService, IDisposable { private readonly byte[] _key = RandomNumberGenerator.GetBytes(32); private bool _disposed; public string CreateDerivationSalt() { ObjectDisposedException.ThrowIf(_disposed, this); byte[] salt = RandomNumberGenerator.GetBytes(32); try { return Encode(salt); } finally { CryptographicOperations.ZeroMemory(salt); } } public string DeriveCapability( string purpose, string ownerSubject, string idempotencyKey, string requestFingerprint, string derivationSalt) { byte[] digest = Derive( purpose, ownerSubject, idempotencyKey, requestFingerprint, derivationSalt); try { return Encode(digest); } finally { CryptographicOperations.ZeroMemory(digest); } } public Guid DeriveGuid( string purpose, string ownerSubject, string idempotencyKey, string requestFingerprint, string derivationSalt) { byte[] digest = Derive( purpose, ownerSubject, idempotencyKey, requestFingerprint, derivationSalt); try { Span guidBytes = digest.AsSpan(0, 16); guidBytes[7] = (byte)((guidBytes[7] & 0x0f) | 0x80); guidBytes[8] = (byte)((guidBytes[8] & 0x3f) | 0x80); return new Guid(guidBytes); } finally { CryptographicOperations.ZeroMemory(digest); } } public string DeriveOpaqueIdentifier(string purpose, string value) { byte[] digest = Derive(purpose, value); try { return Encode(digest); } finally { CryptographicOperations.ZeroMemory(digest); } } public bool TryFingerprint(string? capability, out SecretFingerprint fingerprint) { fingerprint = default; if (_disposed || capability is null || capability.Length != 43 || capability.Any(static character => character is not (>= 'A' and <= 'Z') and not (>= 'a' and <= 'z') and not (>= '0' and <= '9') and not '-' and not '_')) { return false; } byte[] digest = Derive("fingerprint", capability); try { fingerprint = new SecretFingerprint(Encode(digest)); return true; } finally { CryptographicOperations.ZeroMemory(digest); } } public void Dispose() { if (_disposed) { return; } _disposed = true; CryptographicOperations.ZeroMemory(_key); } public override string ToString() => "[EphemeralCapabilityIssuer: key and capabilities redacted]"; private byte[] Derive(params string[] segments) { ObjectDisposedException.ThrowIf(_disposed, this); using IncrementalHash hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, _key); Span length = stackalloc byte[sizeof(int)]; foreach (string segment in segments) { ArgumentException.ThrowIfNullOrEmpty(segment); byte[] encoded = Encoding.UTF8.GetBytes(segment); BinaryPrimitives.WriteInt32BigEndian(length, encoded.Length); hmac.AppendData(length); hmac.AppendData(encoded); CryptographicOperations.ZeroMemory(encoded); } return hmac.GetHashAndReset(); } private static string Encode(ReadOnlySpan bytes) => Convert .ToBase64String(bytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); }