100 lines
3.5 KiB
C#
100 lines
3.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using static StellaOps.Localization.T;
|
|
|
|
namespace StellaOps.Cryptography.Kms;
|
|
|
|
public sealed partial class FileKmsClient
|
|
{
|
|
public async Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
|
|
|
|
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: true).ConfigureAwait(false)
|
|
?? throw new InvalidOperationException(_t("crypto.kms.metadata_failed"));
|
|
|
|
if (record.State == KmsKeyState.Revoked)
|
|
{
|
|
throw new InvalidOperationException(_t("crypto.kms.key_revoked", keyId));
|
|
}
|
|
|
|
var timestamp = _timeProvider.GetUtcNow();
|
|
var versionId = $"{timestamp:yyyyMMddTHHmmssfffZ}";
|
|
var keyData = CreateKeyMaterial(record.Algorithm);
|
|
|
|
try
|
|
{
|
|
var envelope = EncryptPrivateKey(keyData.PrivateBlob);
|
|
var fileName = $"{versionId}.key.json";
|
|
var keyPath = Path.Combine(GetKeyDirectory(keyId), fileName);
|
|
await WriteJsonAsync(keyPath, envelope, cancellationToken).ConfigureAwait(false);
|
|
|
|
foreach (var existing in record.Versions.Where(v => v.State == KmsKeyState.Active))
|
|
{
|
|
existing.State = KmsKeyState.PendingRotation;
|
|
}
|
|
|
|
record.Versions.Add(new KeyVersionRecord
|
|
{
|
|
VersionId = versionId,
|
|
State = KmsKeyState.Active,
|
|
CreatedAt = timestamp,
|
|
PublicKey = keyData.PublicKey,
|
|
CurveName = keyData.Curve,
|
|
FileName = fileName,
|
|
});
|
|
|
|
record.CreatedAt ??= timestamp;
|
|
record.State = KmsKeyState.Active;
|
|
record.ActiveVersion = versionId;
|
|
|
|
await SaveMetadataAsync(record, cancellationToken).ConfigureAwait(false);
|
|
return ToMetadata(record);
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(keyData.PrivateBlob);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_mutex.Release();
|
|
}
|
|
}
|
|
|
|
public async Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
|
|
|
|
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false)
|
|
?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
|
|
|
var timestamp = _timeProvider.GetUtcNow();
|
|
record.State = KmsKeyState.Revoked;
|
|
foreach (var version in record.Versions)
|
|
{
|
|
if (version.State != KmsKeyState.Revoked)
|
|
{
|
|
version.State = KmsKeyState.Revoked;
|
|
version.DeactivatedAt = timestamp;
|
|
}
|
|
}
|
|
|
|
await SaveMetadataAsync(record, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
_mutex.Release();
|
|
}
|
|
}
|
|
} |