// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using AWS.EncryptionSDK; using AWS.EncryptionSDK.Core; using Xunit; using static ExampleUtils.ExampleUtils; using static ExampleUtils.WriteExampleResources; /// Demonstrates using a Custom Client Supplier. /// See RegionalRoleClientSupplier.cs for the details of implementing a /// custom client supplier. /// This example uses an AwsKmsMrkDiscoveryMultiKeyring, but all /// the AWS Multi Keyrings take Client Suppliers. public class ClientSupplierExample { private const string FILE_NAME = "defaultRegionMrkKey.bin"; private static void Run(MemoryStream plaintext, List accountIds, List regions) { // Instantiate the Material Providers and the AWS Encryption SDK var materialProviders = AwsCryptographicMaterialProvidersFactory.CreateDefaultAwsCryptographicMaterialProviders(); var encryptionSdk = AwsEncryptionSdkFactory.CreateDefaultAwsEncryptionSdk(); /* 1. Generate or load a ciphertext encrypted by the KMS Key. */ // To focus on Client Suppliers, we will rely on a helper method // to provide the encrypted message (ciphertext). var ciphertext = ReadMessage(FILE_NAME); var encryptionContext = GetEncryptionContext(); /* 2. Create a KMS Multi Keyring with the `RegionalRoleClientSupplier` */ // Now create a Discovery keyring to use for decryption. // We are passing in our Custom Client Supplier. var createDecryptKeyringInput = new CreateAwsKmsMrkDiscoveryMultiKeyringInput { ClientSupplier = new RegionalRoleClientSupplier(), Regions = regions, DiscoveryFilter = new DiscoveryFilter() { AccountIds = accountIds, Partition = "aws" } }; // This is a Multi Keyring composed of MRK Discovery Keyrings. // All the keyrings have the same Discovery Filter. // Each keyring has its own KMS Client, which is provisioned by the Custom Client Supplier. var multiKeyring = materialProviders.CreateAwsKmsMrkDiscoveryMultiKeyring(createDecryptKeyringInput); /* 3. Decrypt the ciphertext with created KMS Multi Keyring */ // On Decrypt, the header of the encrypted message (ciphertext) will be parsed. // The header contains the Encrypted Data Keys (EDKs), which, if the EDK // was encrypted by a KMS Keyring, includes the KMS Key arn. // For each member of the Multi Keyring, every EDK will try to be decrypted until a decryption is successful. // Since every member of the Multi Keyring is a MRK Discovery Keyring: // Each Keyring will filter the EDKs by the Discovery Filter and the keyring's region. // For each filtered EDK, the keyring will attempt decryption with the keyring's client. // All of this is done serially, until a success occurs or all keyrings have failed all (filtered) EDKs. // KMS MRK Discovery Keyrings will attempt to decrypt Multi Region Keys (MRKs) and regular KMS Keys. var decryptInput = new DecryptInput { Ciphertext = ciphertext, Keyring = multiKeyring }; var decryptOutput = encryptionSdk.Decrypt(decryptInput); /* 4. Verify the encryption context */ VerifyEncryptionContext(decryptOutput, encryptionContext); /* 5. Verify the decrypted plaintext is the same as the original */ VerifyDecryptedIsPlaintext(decryptOutput, plaintext); /* 6. Test the Missing Region Exception */ // Demonstrate catching a custom exception. var createMultiFailed = false; createDecryptKeyringInput.Regions = new List() {"fake-region"}; try { materialProviders.CreateAwsKmsMrkDiscoveryMultiKeyring(createDecryptKeyringInput); } // Note that the exception returned is NOT a `MissingRegionException` catch (MissingRegionException) { throw; } // But is cast down to an `AwsCryptographicMaterialProvidersBaseException`. catch (AwsCryptographicMaterialProvidersBaseException exception) { // However, the message is as expected. Assert.Equal( "Region fake-region is not supported by this client supplier", exception.Message); createMultiFailed = true; } finally { Assert.True(createMultiFailed); } } /// /// For this example, we break out the Encryption Context Verification /// into a helper method. /// While Encryption Context Verification is a best practice, it is not /// the topic of this example. /// private static void VerifyEncryptionContext( DecryptOutput decryptOutput, Dictionary encryptionContext ) { // Before your application uses plaintext data, verify that the encryption context that // you used to encrypt the message is included in the encryption context that was used to // decrypt the message. The AWS Encryption SDK can add pairs, so don't require an exact match. // // In production, always use a meaningful encryption context. foreach (var expectedPair in encryptionContext) if (!decryptOutput.EncryptionContext.TryGetValue(expectedPair.Key, out var decryptedValue) || !decryptedValue.Equals(expectedPair.Value)) throw new Exception("Encryption context does not match expected values"); } /// /// This is helper method that ensures the decrypted message is the /// same as the encrypted message. /// private static void VerifyDecryptedIsPlaintext(DecryptOutput decryptOutput, MemoryStream plaintext) { var decrypted = decryptOutput.Plaintext; Assert.Equal(decrypted.ToArray(), plaintext.ToArray()); } // We test examples to ensure they remain up-to-date. [Fact] public void TestClientSupplierExample() { if (!File.Exists(GetResourcePath(FILE_NAME))) { EncryptAndWrite(GetPlaintextStream(), GetDefaultRegionMrkKeyArn(), FILE_NAME); } Run( GetPlaintextStream(), GetAccountIds(), GetRegionIAMRoleMap().Keys.ToList() ); } }