Files
Rendezvous/src/FinalFactory.Rendezvous.Server/Provisioning/SecretProviders.cs
T
KyuubiYoru 08729ae25c
quality-gate / quality (push) Failing after 1m9s
quality-gate / container (push) Has been skipped
feat(deployment): add secure Linux runtime (#17)
2026-07-16 15:03:04 +02:00

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]";
}