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 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(); } } }