using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; using SwBLL.OfflineRollout; using Xunit; namespace SwTests; /// /// Covers RolloutPackageCrypto (Offline Rollout Package, tracker ยง53) โ€” real round-trip proof, /// not mocked: generates a throwaway RSA keypair, encrypts via the real class, then decrypts /// inline using the exact inverse of the documented container format (the same format /// FrameworkBLL.Promotion.PromotionPackageCrypto's own DecryptPackageAsync implements โ€” this test /// deliberately reimplements decrypt rather than referencing that class, since GB5Framework isn't /// a dependency of SwTests and the whole point of this format being documented/versioned is that /// an independent reader can decrypt it correctly from the spec alone). /// public class RolloutPackageCryptoTests { [Fact] public async Task EncryptPackageAsync_RealRoundTrip_DecryptsToOriginalPlaintext() { using var rsa = RSA.Create(2048); var publicKeyPem = rsa.ExportSubjectPublicKeyInfoPem(); var plaintext = Encoding.UTF8.GetBytes("{\"PackageCode\":\"TEST_PKG\",\"Scripts\":[]}"); var crypto = new RolloutPackageCrypto(); var encrypted = await crypto.EncryptPackageAsync(plaintext, publicKeyPem, default); var decrypted = DecryptForTest(encrypted, rsa); Assert.Equal(plaintext, decrypted); } [Fact] public async Task EncryptPackageAsync_EmptyPlaintext_Throws() { var crypto = new RolloutPackageCrypto(); await Assert.ThrowsAsync(() => crypto.EncryptPackageAsync(Array.Empty(), "PEM", default)); } [Fact] public async Task EncryptPackageAsync_MissingPublicKey_Throws() { var crypto = new RolloutPackageCrypto(); await Assert.ThrowsAsync(() => crypto.EncryptPackageAsync(new byte[] { 1 }, "", default)); } // Inverse of RolloutPackageCrypto.EncryptPackageAsync's documented container format โ€” // deliberately independent of the class under test, so this proves the *format* round-trips // correctly, not just that encrypt/decrypt happen to agree with each other's internal state. private static byte[] DecryptForTest(byte[] encryptedPackage, RSA rsa) { using var input = new MemoryStream(encryptedPackage); using var reader = new BinaryReader(input); var formatVersion = reader.ReadByte(); Assert.Equal(1, formatVersion); var encryptedAesKeyLength = BinaryPrimitives.ReverseEndianness(reader.ReadInt32()); var encryptedAesKey = reader.ReadBytes(encryptedAesKeyLength); var nonce = reader.ReadBytes(12); var tag = reader.ReadBytes(16); var ciphertext = reader.ReadBytes((int)(input.Length - input.Position)); var aesKey = rsa.Decrypt(encryptedAesKey, RSAEncryptionPadding.OaepSHA256); var plaintext = new byte[ciphertext.Length]; using (var aes = new AesGcm(aesKey, 16)) aes.Decrypt(nonce, ciphertext, tag, plaintext); return plaintext; } }