Files
git.stella-ops.org/src/__Libraries/StellaOps.Cryptography.Kms/FileKmsClient.Import.cs

107 lines
4.1 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
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> ImportAsync(
string keyId,
KmsKeyMaterial material,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
ArgumentNullException.ThrowIfNull(material);
if (material.D is null || material.D.Length == 0)
{
throw new ArgumentException("Key material must include private key bytes.", nameof(material));
}
if (material.Qx is null || material.Qx.Length == 0 || material.Qy is null || material.Qy.Length == 0)
{
throw new ArgumentException("Key material must include public key coordinates.", nameof(material));
}
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 (!string.Equals(record.Algorithm, material.Algorithm, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(_t("crypto.kms.algorithm_mismatch", record.Algorithm, material.Algorithm));
}
var versionId = string.IsNullOrWhiteSpace(material.VersionId)
? $"{_timeProvider.GetUtcNow():yyyyMMddTHHmmssfffZ}"
: material.VersionId;
if (record.Versions.Any(v => string.Equals(v.VersionId, versionId, StringComparison.Ordinal)))
{
throw new InvalidOperationException(_t("crypto.kms.version_exists", versionId, record.KeyId));
}
var curveName = string.IsNullOrWhiteSpace(material.Curve) ? "nistP256" : material.Curve;
ResolveCurve(curveName); // validate supported curve
var privateKeyRecord = new EcdsaPrivateKeyRecord
{
Curve = curveName,
D = Convert.ToBase64String(material.D),
Qx = Convert.ToBase64String(material.Qx),
Qy = Convert.ToBase64String(material.Qy),
};
var privateBlob = JsonSerializer.SerializeToUtf8Bytes(privateKeyRecord, _jsonOptions);
try
{
var envelope = EncryptPrivateKey(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;
}
var createdAt = material.CreatedAt == default ? _timeProvider.GetUtcNow() : material.CreatedAt;
var publicKey = CombinePublicCoordinates(material.Qx, material.Qy);
record.Versions.Add(new KeyVersionRecord
{
VersionId = versionId,
State = KmsKeyState.Active,
CreatedAt = createdAt,
PublicKey = Convert.ToBase64String(publicKey),
CurveName = curveName,
FileName = fileName,
});
record.CreatedAt ??= createdAt;
record.State = KmsKeyState.Active;
record.ActiveVersion = versionId;
await SaveMetadataAsync(record, cancellationToken).ConfigureAwait(false);
return ToMetadata(record);
}
finally
{
CryptographicOperations.ZeroMemory(privateBlob);
}
}
finally
{
_mutex.Release();
}
}
}