forked from xiongyuxing/tiku-backend.net
117 lines
3.7 KiB
C#
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}");
|
|
}
|