206 lines
5.7 KiB
C#
206 lines
5.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 EnvironmentPrefix = "env:";
|
|
private const string FilePrefix = "file:";
|
|
private const int MaximumSecretBytes = 4096;
|
|
|
|
public bool TryGetSecret(string reference, out SecretMaterial? secret)
|
|
{
|
|
secret = null;
|
|
if (reference.StartsWith(EnvironmentPrefix, StringComparison.Ordinal)
|
|
&& reference.Length > EnvironmentPrefix.Length)
|
|
{
|
|
return TryGetEnvironmentSecret(reference[EnvironmentPrefix.Length..], out secret);
|
|
}
|
|
|
|
if (reference.StartsWith(FilePrefix, StringComparison.Ordinal)
|
|
&& reference.Length > FilePrefix.Length)
|
|
{
|
|
return TryGetFileSecret(reference[FilePrefix.Length..], out secret);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryGetEnvironmentSecret(string variableName, out SecretMaterial? secret)
|
|
{
|
|
secret = null;
|
|
string? encoded = Environment.GetEnvironmentVariable(variableName);
|
|
if (string.IsNullOrEmpty(encoded))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
byte[] bytes = Convert.FromBase64String(encoded);
|
|
if (bytes.Length is 0 or > MaximumSecretBytes)
|
|
{
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
return false;
|
|
}
|
|
|
|
secret = new SecretMaterial(bytes);
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
return true;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryGetFileSecret(string path, out SecretMaterial? secret)
|
|
{
|
|
secret = null;
|
|
byte[]? bytes = null;
|
|
try
|
|
{
|
|
FileInfo file = new(path);
|
|
if (!file.Exists
|
|
|| !Path.IsPathFullyQualified(path)
|
|
|| file.LinkTarget is not null
|
|
|| file.Length is <= 0 or > MaximumSecretBytes)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bytes = File.ReadAllBytes(path);
|
|
if (bytes.Length is 0 or > MaximumSecretBytes)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
secret = new SecretMaterial(bytes);
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (exception is IOException
|
|
or UnauthorizedAccessException
|
|
or ArgumentException
|
|
or NotSupportedException
|
|
or System.Security.SecurityException)
|
|
{
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
if (bytes is not null)
|
|
{
|
|
CryptographicOperations.ZeroMemory(bytes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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]";
|
|
}
|