// 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 Amazon.KeyManagementService;
using AWS.EncryptionSDK;
using AWS.EncryptionSDK.Core;
using Xunit;
using static ExampleUtils.ExampleUtils;
using static ExampleUtils.WriteExampleResources;
/// Demonstrate using Discovery Filters.
///
/// Discovery Filters are used to restrict Discovery Keyrings
/// to trusted AWS Accounts.
/// The Accounts are specified by their Account Ids
/// and the partition they are in.
///
/// It's always a best practice to specify your wrapping keys explicitly.
/// This practice assures that you only use the keys that you intend.
/// It also improves performance by preventing you from
/// inadvertently using keys in a different AWS account or Region,
/// or attempting to decrypt with keys that you don't have permission to use.
///
/// However, when decrypting with AWS KMS keyrings,
/// you are not required to specify wrapping keys.
/// The AWS Encryption SDK can get the key identifier
/// from the metadata of the encrypted data key.
///
/// When specifying AWS KMS wrapping keys for decrypting is impractical
/// (such as when encrypting using AWS KMS Aliases),
/// you can use discovery keyrings.
///
/// When you can not specify your wrapping keys explicitly,
/// using a Discovery Filter is a best practice.
///
/// Particularly if an application is decrypting messages from multiple sources,
/// adding trusted AWS accounts to the discovery filter allows it to
/// protect itself from decrypting messages from untrusted sources.
public class DiscoveryFilterExample
{
const string fileName = "defaultRegionKmsKey.bin";
/// unencrypted data
/// List of AWS Account Ids that are trusted.
/// AWS Partition that contains all the members of "trustedAccountIds".
///
private static void Run(
MemoryStream plaintext,
List trustedAccountIds,
string awsPartition
)
{
/* 1. Instantiate the Material Providers and Encryption SDK */
var materialProviders =
AwsCryptographicMaterialProvidersFactory.CreateDefaultAwsCryptographicMaterialProviders();
// Instantiate the Encryption SDK such that it limits the number of
// Encrypted Data Keys a ciphertext may contain.
// Discovery Keyrings are an excellent tool
// for handling encrypted messages from multiple sources.
// Limiting the number of encrypted data keys is a best practice,
// particularly when decrypting messages from multiple sources.
// See the LimitEncryptedDataKeysExample for details.
var esdkConfig = new AwsEncryptionSdkConfig
{
MaxEncryptedDataKeys = 1
};
var encryptionSdk = AwsEncryptionSdkFactory.CreateAwsEncryptionSdk(esdkConfig);
/* 2. Create a Discovery Keyring with a Discovery Filter */
// We create a Discovery keyring to use for decryption.
// We'll add a discovery filter so that we limit the set of Encrypted Data Keys
// we are willing to decrypt to only ones created by KMS keys from
// trusted accounts.
var decryptKeyringInput = new CreateAwsKmsDiscoveryKeyringInput
{
KmsClient = new AmazonKeyManagementServiceClient(),
DiscoveryFilter = new DiscoveryFilter
{
AccountIds = trustedAccountIds,
Partition = awsPartition
}
};
var decryptKeyring = materialProviders.CreateAwsKmsDiscoveryKeyring(decryptKeyringInput);
/* 3. Retrieve or create an encrypted message to decrypt. */
// To focus on Discovery Filters,
// we rely on a helper method to load the encrypted message.
var ciphertext = ReadMessage(fileName);
Dictionary encryptionContext = GetEncryptionContext();
/* 4. Decrypt the encrypted data. */
var decryptInput = new DecryptInput
{
Ciphertext = ciphertext,
Keyring = decryptKeyring
};
var decryptOutput = encryptionSdk.Decrypt(decryptInput);
/* 5. Verify the encryption context */
VerifyEncryptionContext(decryptOutput, encryptionContext);
/* 6. Verify the decrypted plaintext is the original plaintext */
VerifyDecryptedIsPlaintext(decryptOutput, plaintext);
/* 7. Create a discovery filter that excludes the encrypted data key */
// If we create a Discovery Filter that excludes
// all the accounts the ciphertext was encrypted with,
// the decryption will fail.
decryptKeyringInput.DiscoveryFilter = new DiscoveryFilter
{
AccountIds = new List {"123456789012"},
Partition = awsPartition
};
/* 8. Validate the excluding discovery filter fails to decrypt the ciphertext */
var decryptFailed = false;
var failingKeyring = materialProviders.CreateAwsKmsDiscoveryKeyring(decryptKeyringInput);
decryptInput.Keyring = failingKeyring;
try
{
encryptionSdk.Decrypt(decryptInput);
}
catch (AwsEncryptionSdkException)
{
decryptFailed = true;
}
Assert.True(decryptFailed);
}
///
/// For this example, we break out 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 helper method ensures the decrypted message is the same as the
/// encrypted message.
///
private static void VerifyDecryptedIsPlaintext(DecryptOutput decryptOutput, MemoryStream plaintext)
{
// Demonstrate that the decrypted plaintext is identical to the original plaintext.
var decrypted = decryptOutput.Plaintext;
Assert.Equal(decrypted.ToArray(), plaintext.ToArray());
}
// We test examples to ensure they remain up-to-date.
[Fact]
public void TestDiscoveryFilterExample()
{
if (!File.Exists(GetResourcePath(fileName)))
{
EncryptAndWrite(GetPlaintextStream(), GetDefaultRegionKmsKeyArn(), fileName);
}
Run(GetPlaintextStream(), GetAccountIds(), "aws");
}
}