using System; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using FrameworkDAL.CustomCode.Promotion; using GB5Shared.DTO.Framework.Login; using GB5Shared.Vault; using Microsoft.Extensions.Configuration; namespace FrameworkBLL.Promotion { // ============================================================ // PromotionPackageCrypto — see IPromotionPackageCrypto. // // Container format (versioned so a future scheme change doesn't break // reading old packages): // [1 byte] FormatVersion (=1) // [4 bytes] BE length of the RSA-OAEP-SHA256-encrypted AES key (N) // [N bytes] RSA-encrypted AES-256 key // [12 bytes] AES-GCM nonce // [16 bytes] AES-GCM tag // [remaining bytes] AES-GCM ciphertext // // This deployment's own private key lives in Vault (GB5Shared.Vault, // the only supported way any GB5 module touches Vault) under the key // named by config "Promotion:PrivateKeyVaultKey" — never accepted from a // request, same rule GopQueueBLL.SubmitExecution already follows for // GOP:Environment. // ============================================================ public class PromotionPackageCrypto : IPromotionPackageCrypto { private const byte FormatVersion = 1; private const int AesKeySizeBytes = 32; // AES-256 private const int NonceSizeBytes = 12; // AES-GCM standard nonce size private const int TagSizeBytes = 16; // AES-GCM standard tag size private readonly IPromotionCounterpartyDAL _CounterpartyDal; private readonly IVaultService _VaultService; private readonly IConfiguration _Configuration; public PromotionPackageCrypto( IPromotionCounterpartyDAL counterpartyDal, IVaultService vaultService, IConfiguration configuration) { _CounterpartyDal = counterpartyDal; _VaultService = vaultService; _Configuration = configuration; } public async Task EncryptPackageAsync( byte[] plaintext, string destinationEnvironmentCode, LoginDTO login, CancellationToken ct) { var counterparty = await _CounterpartyDal.GetByEnvironmentCode( destinationEnvironmentCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No active counterparty registered for '{destinationEnvironmentCode}' — " + "its public key must be registered in MPROMOTIONCOUNTERPARTY before a package " + "can be encrypted to it."); using var rsa = RSA.Create(); rsa.ImportFromPem(counterparty.PublicKeyPem); var aesKey = RandomNumberGenerator.GetBytes(AesKeySizeBytes); var nonce = RandomNumberGenerator.GetBytes(NonceSizeBytes); var ciphertext = new byte[plaintext.Length]; var tag = new byte[TagSizeBytes]; using (var aes = new AesGcm(aesKey, TagSizeBytes)) aes.Encrypt(nonce, plaintext, ciphertext, tag); var encryptedAesKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256); using var output = new MemoryStream(); using (var writer = new BinaryWriter(output)) { writer.Write(FormatVersion); writer.Write(System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(encryptedAesKey.Length)); writer.Write(encryptedAesKey); writer.Write(nonce); writer.Write(tag); writer.Write(ciphertext); } return output.ToArray(); } public async Task DecryptPackageAsync(byte[] encryptedPackage, CancellationToken ct) { using var input = new MemoryStream(encryptedPackage); using var reader = new BinaryReader(input); var formatVersion = reader.ReadByte(); if (formatVersion != FormatVersion) throw new NotSupportedException( $"Promotion package format version {formatVersion} is not supported by this deployment."); var encryptedAesKeyLength = System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(reader.ReadInt32()); var encryptedAesKey = reader.ReadBytes(encryptedAesKeyLength); var nonce = reader.ReadBytes(NonceSizeBytes); var tag = reader.ReadBytes(TagSizeBytes); var ciphertext = reader.ReadBytes((int)(input.Length - input.Position)); var privateKeyVaultKey = _Configuration["Promotion:PrivateKeyVaultKey"] ?? throw new InvalidOperationException( "Promotion:PrivateKeyVaultKey is not configured on this deployment."); var privateKeyPem = await _VaultService.GetSecretAsync(privateKeyVaultKey, ct).ConfigureAwait(false); using var rsa = RSA.Create(); rsa.ImportFromPem(privateKeyPem); var aesKey = rsa.Decrypt(encryptedAesKey, RSAEncryptionPadding.OaepSHA256); var plaintext = new byte[ciphertext.Length]; using (var aes = new AesGcm(aesKey, TagSizeBytes)) aes.Decrypt(nonce, ciphertext, tag, plaintext); return plaintext; } } }