145 lines
3.7 KiB
C#
145 lines
3.7 KiB
C#
using System.Security.Cryptography;
|
|
|
|
namespace FinalFactory.Rendezvous.Server.Provisioning;
|
|
|
|
internal interface ISecretProvider
|
|
{
|
|
bool TryGetSecret(string reference, out SecretMaterial? secret);
|
|
}
|
|
|
|
internal sealed class SecretMaterial : IDisposable
|
|
{
|
|
private byte[]? _bytes;
|
|
|
|
public SecretMaterial(ReadOnlySpan<byte> bytes)
|
|
{
|
|
if (bytes.Length == 0)
|
|
{
|
|
throw new ArgumentException("Secret material cannot be empty.", nameof(bytes));
|
|
}
|
|
|
|
_bytes = bytes.ToArray();
|
|
}
|
|
|
|
public int Length => _bytes?.Length ?? 0;
|
|
|
|
public byte[] CopyBytes() => _bytes?.ToArray()
|
|
?? throw new ObjectDisposedException(nameof(SecretMaterial));
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_bytes is not null)
|
|
{
|
|
CryptographicOperations.ZeroMemory(_bytes);
|
|
_bytes = null;
|
|
}
|
|
}
|
|
|
|
public override string ToString() => "[REDACTED SECRET]";
|
|
}
|
|
|
|
internal sealed class EnvironmentSecretProvider : ISecretProvider
|
|
{
|
|
private const string Prefix = "env:";
|
|
|
|
public bool TryGetSecret(string reference, out SecretMaterial? secret)
|
|
{
|
|
secret = null;
|
|
if (!reference.StartsWith(Prefix, StringComparison.Ordinal)
|
|
|| reference.Length == Prefix.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string? encoded = Environment.GetEnvironmentVariable(reference[Prefix.Length..]);
|
|
if (string.IsNullOrEmpty(encoded))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
byte[] bytes = Convert.FromBase64String(encoded);
|
|
secret = new SecretMaterial(bytes);
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
return true;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class EphemeralDevelopmentSecretProvider : ISecretProvider, IDisposable
|
|
{
|
|
private const string Prefix = "development:ephemeral/";
|
|
private readonly Dictionary<string, byte[]> _secrets = new(StringComparer.Ordinal);
|
|
|
|
public bool TryGetSecret(string reference, out SecretMaterial? secret)
|
|
{
|
|
secret = null;
|
|
if (!reference.StartsWith(Prefix, StringComparison.Ordinal)
|
|
|| reference.Length == Prefix.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_secrets.TryGetValue(reference, out byte[]? bytes))
|
|
{
|
|
bytes = RandomNumberGenerator.GetBytes(32);
|
|
_secrets.Add(reference, bytes);
|
|
}
|
|
|
|
secret = new SecretMaterial(bytes);
|
|
return true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (byte[] bytes in _secrets.Values)
|
|
{
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
}
|
|
|
|
_secrets.Clear();
|
|
}
|
|
|
|
public override string ToString() => "[EphemeralDevelopmentSecretProvider]";
|
|
}
|
|
|
|
internal sealed class DictionarySecretProvider : ISecretProvider, IDisposable
|
|
{
|
|
private readonly Dictionary<string, byte[]> _secrets;
|
|
|
|
public DictionarySecretProvider(IReadOnlyDictionary<string, byte[]> secrets) =>
|
|
_secrets = secrets.ToDictionary(
|
|
static item => item.Key,
|
|
static item => item.Value.ToArray(),
|
|
StringComparer.Ordinal);
|
|
|
|
public bool TryGetSecret(string reference, out SecretMaterial? secret)
|
|
{
|
|
if (_secrets.TryGetValue(reference, out byte[]? bytes))
|
|
{
|
|
secret = new SecretMaterial(bytes);
|
|
return true;
|
|
}
|
|
|
|
secret = null;
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (byte[] bytes in _secrets.Values)
|
|
{
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
}
|
|
|
|
_secrets.Clear();
|
|
}
|
|
|
|
public override string ToString() => "[DictionarySecretProvider: REDACTED]";
|
|
}
|