Files
tiku-backend.net/Tiku.Infrastructure/Commerce/TenantSecretEncryption.cs

117 lines
3.7 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Tiku.Infrastructure.Commerce;
public sealed class TenantSecretEncryptionOptions
{
public const string SectionName = "Security:TenantSecrets";
public const string DevelopmentMasterKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
public string KeyId { get; set; } = string.Empty;
public string MasterKey { get; set; } = string.Empty;
public static bool BeValid(TenantSecretEncryptionOptions options)
{
if (string.IsNullOrWhiteSpace(options.KeyId) || string.IsNullOrWhiteSpace(options.MasterKey))
{
return false;
}
try
{
return Convert.FromBase64String(options.MasterKey).Length == 32;
}
catch (FormatException)
{
return false;
}
}
public static bool IsDevelopmentDefault(TenantSecretEncryptionOptions options) =>
string.Equals(options.MasterKey, DevelopmentMasterKey, StringComparison.Ordinal);
}
internal interface ITenantSecretProtector
{
ProtectedTenantSecret Protect(Guid tenantId, string secretRef, JsonElement payload);
JsonElement Unprotect(
Guid tenantId,
string secretRef,
string keyId,
byte[] ciphertext,
byte[] nonce,
byte[] tag);
}
internal sealed record ProtectedTenantSecret(
string KeyId,
byte[] Ciphertext,
byte[] Nonce,
byte[] Tag);
internal sealed class TenantSecretProtector(
Microsoft.Extensions.Options.IOptions<TenantSecretEncryptionOptions> options) : ITenantSecretProtector
{
private readonly TenantSecretEncryptionOptions options = options.Value;
public ProtectedTenantSecret Protect(Guid tenantId, string secretRef, JsonElement payload)
{
var key = Convert.FromBase64String(options.MasterKey);
var plaintext = JsonSerializer.SerializeToUtf8Bytes(payload);
var nonce = RandomNumberGenerator.GetBytes(12);
var ciphertext = new byte[plaintext.Length];
var tag = new byte[16];
var associatedData = GetAssociatedData(tenantId, secretRef, options.KeyId);
try
{
using var aes = new AesGcm(key, tag.Length);
aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData);
return new ProtectedTenantSecret(options.KeyId, ciphertext, nonce, tag);
}
finally
{
CryptographicOperations.ZeroMemory(key);
CryptographicOperations.ZeroMemory(plaintext);
}
}
public JsonElement Unprotect(
Guid tenantId,
string secretRef,
string keyId,
byte[] ciphertext,
byte[] nonce,
byte[] tag)
{
if (!string.Equals(keyId, options.KeyId, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Tenant secret uses unknown encryption key '{keyId}'.");
}
var key = Convert.FromBase64String(options.MasterKey);
var plaintext = new byte[ciphertext.Length];
var associatedData = GetAssociatedData(tenantId, secretRef, keyId);
try
{
using var aes = new AesGcm(key, tag.Length);
aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData);
using var document = JsonDocument.Parse(plaintext);
return document.RootElement.Clone();
}
finally
{
CryptographicOperations.ZeroMemory(key);
CryptographicOperations.ZeroMemory(plaintext);
}
}
private static byte[] GetAssociatedData(Guid tenantId, string secretRef, string keyId) =>
Encoding.UTF8.GetBytes($"{tenantId:N}\n{secretRef}\n{keyId}");
}