107 lines
4.1 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|