173 lines
4.7 KiB
C#
173 lines
4.7 KiB
C#
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<byte> 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<byte> 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<byte> bytes) => Convert
|
|
.ToBase64String(bytes)
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_');
|
|
}
|