feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s
quality-gate / quality (push) Successful in 55s
Closes #7
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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);
|
||||
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 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('/', '_');
|
||||
}
|
||||
Reference in New Issue
Block a user