using Google.Cloud.Kms.V1; using Google.Protobuf; using Google.Protobuf.WellKnownTypes; namespace StellaOps.Cryptography.Kms; public interface IGcpKmsFacade : IDisposable { Task SignAsync(string versionName, ReadOnlyMemory digest, CancellationToken cancellationToken); Task GetCryptoKeyMetadataAsync(string keyName, CancellationToken cancellationToken); Task> ListKeyVersionsAsync(string keyName, CancellationToken cancellationToken); Task GetPublicKeyAsync(string versionName, CancellationToken cancellationToken); } public sealed record GcpSignResult(string VersionName, byte[] Signature); public sealed record GcpCryptoKeyMetadata(string KeyName, string? PrimaryVersionName, DateTimeOffset CreateTime); public enum GcpCryptoKeyVersionState { Unspecified = 0, PendingGeneration = 1, Enabled = 2, Disabled = 3, DestroyScheduled = 4, Destroyed = 5, PendingImport = 6, ImportFailed = 7, GenerationFailed = 8, } public sealed record GcpCryptoKeyVersionMetadata( string VersionName, GcpCryptoKeyVersionState State, DateTimeOffset CreateTime, DateTimeOffset? DestroyTime); public sealed record GcpPublicKeyMaterial(string VersionName, string Algorithm, string Pem); internal sealed class GcpKmsFacade : IGcpKmsFacade { private readonly KeyManagementServiceClient _client; private readonly bool _ownsClient; public GcpKmsFacade(GcpKmsOptions options) { ArgumentNullException.ThrowIfNull(options); var builder = new KeyManagementServiceClientBuilder { Endpoint = string.IsNullOrWhiteSpace(options.Endpoint) ? KeyManagementServiceClient.DefaultEndpoint : options.Endpoint, }; _client = builder.Build(); _ownsClient = true; } public GcpKmsFacade(KeyManagementServiceClient client) { _client = client ?? throw new ArgumentNullException(nameof(client)); _ownsClient = false; } public async Task SignAsync(string versionName, ReadOnlyMemory digest, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(versionName); var response = await _client.AsymmetricSignAsync(new AsymmetricSignRequest { Name = versionName, Digest = new Digest { Sha256 = ByteString.CopyFrom(digest.ToArray()), }, }, cancellationToken).ConfigureAwait(false); return new GcpSignResult(response.Name ?? versionName, response.Signature.ToByteArray()); } public async Task GetCryptoKeyMetadataAsync(string keyName, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(keyName); var response = await _client.GetCryptoKeyAsync(new GetCryptoKeyRequest { Name = keyName, }, cancellationToken).ConfigureAwait(false); return new GcpCryptoKeyMetadata( response.Name, response.Primary?.Name, ToDateTimeOffsetOrUtcNow(response.CreateTime)); } public async Task> ListKeyVersionsAsync(string keyName, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(keyName); var results = new List(); var request = new ListCryptoKeyVersionsRequest { Parent = keyName, }; await foreach (var version in _client.ListCryptoKeyVersionsAsync(request).WithCancellation(cancellationToken).ConfigureAwait(false)) { results.Add(new GcpCryptoKeyVersionMetadata( version.Name, MapState(version.State), ToDateTimeOffsetOrUtcNow(version.CreateTime), version.DestroyTime is null ? null : ToDateTimeOffsetOrUtcNow(version.DestroyTime))); } return results; } public async Task GetPublicKeyAsync(string versionName, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(versionName); var response = await _client.GetPublicKeyAsync(new GetPublicKeyRequest { Name = versionName, }, cancellationToken).ConfigureAwait(false); return new GcpPublicKeyMaterial( response.Name ?? versionName, response.Algorithm.ToString(), response.Pem); } private static GcpCryptoKeyVersionState MapState(CryptoKeyVersion.Types.CryptoKeyVersionState state) => state switch { CryptoKeyVersion.Types.CryptoKeyVersionState.Enabled => GcpCryptoKeyVersionState.Enabled, CryptoKeyVersion.Types.CryptoKeyVersionState.Disabled => GcpCryptoKeyVersionState.Disabled, CryptoKeyVersion.Types.CryptoKeyVersionState.DestroyScheduled => GcpCryptoKeyVersionState.DestroyScheduled, CryptoKeyVersion.Types.CryptoKeyVersionState.Destroyed => GcpCryptoKeyVersionState.Destroyed, CryptoKeyVersion.Types.CryptoKeyVersionState.PendingGeneration => GcpCryptoKeyVersionState.PendingGeneration, CryptoKeyVersion.Types.CryptoKeyVersionState.PendingImport => GcpCryptoKeyVersionState.PendingImport, CryptoKeyVersion.Types.CryptoKeyVersionState.ImportFailed => GcpCryptoKeyVersionState.ImportFailed, CryptoKeyVersion.Types.CryptoKeyVersionState.GenerationFailed => GcpCryptoKeyVersionState.GenerationFailed, _ => GcpCryptoKeyVersionState.Unspecified, }; public void Dispose() { if (_ownsClient && _client is IDisposable disposable) { disposable.Dispose(); } } private static DateTimeOffset ToDateTimeOffsetOrUtcNow(Timestamp? timestamp) { if (timestamp is null) { return DateTimeOffset.UtcNow; } if (timestamp.Seconds == 0 && timestamp.Nanos == 0) { return DateTimeOffset.UtcNow; } return timestamp.ToDateTimeOffset(); } }