diff --git a/src/examples/README.md b/src/examples/README.md index 19201887b..c8ae480b7 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -54,6 +54,12 @@ We start with AWS KMS examples, then show how to use other wrapping keys. * How to combine AWS KMS with an offline escrow key * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/multi/AwsKmsWithEscrow.java) * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/multi/AwsKmsWithEscrow.java) +* How to reuse data keys across multiple messages + * [with the caching cryptographic materials manager](./java/com/amazonaws/crypto/examples/cryptomaterialsmanager/caching/SimpleCache.java) +* How to restrict algorithm suites + * [with a custom cryptographic materials manager](./java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/AlgorithmSuiteEnforcement.java) +* How to require encryption context fields + * [with a custom cryptographic materials manager](./java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/RequiringEncryptionContextFields.java) ### Keyrings diff --git a/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/caching/SimpleCache.java b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/caching/SimpleCache.java new file mode 100644 index 000000000..e7038d6c8 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/caching/SimpleCache.java @@ -0,0 +1,122 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.cryptomaterialsmanager.caching; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * The default cryptographic materials manager (CMM) + * creates new encryption and decryption materials + * on every call. + * This means every encrypted message is protected by a unique data key, + * but it also means that you need to interact with your key management system + * in order to process any message. + * If this causes performance, operations, or cost issues for you, + * you might benefit from data key caching. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-key-caching.html + *

+ * This example shows how to configure the caching CMM + * to reuse data keys across multiple encrypted messages. + *

+ * In this example, we use an AWS KMS customer master key (CMK), + * but you can use other key management options with the AWS Encryption SDK. + * For examples that demonstrate how to use other key management configurations, + * see the 'keyring' and 'masterkeyprovider' directories. + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class SimpleCache { + + /** + * Demonstrate an encrypt/decrypt cycle using the caching cryptographic materials manager. + * + * @param awsKmsCmk The ARN of an AWS KMS CMK that protects data keys + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsCmk, final byte[] sourcePlaintext) { + // Instantiate the AWS Encryption SDK. + final AwsCrypto awsEncryptionSdk = new AwsCrypto(); + + // Prepare your encryption context. + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + final Map encryptionContext = new HashMap<>(); + encryptionContext.put("encryption", "context"); + encryptionContext.put("is not", "secret"); + encryptionContext.put("but adds", "useful metadata"); + encryptionContext.put("that can help you", "be confident that"); + encryptionContext.put("the data you are handling", "is what you think it is"); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Create the caching cryptographic materials manager using your keyring. + final CryptoMaterialsManager cmm = CachingCryptoMaterialsManager.newBuilder() + .withKeyring(keyring) + // The cache is where the caching CMM stores the materials. + // + // LocalCryptoMaterialsCache gives you a local, in-memory, cache. + .withCache(new LocalCryptoMaterialsCache(100)) + // Max Age determines how long the caching CMM will reuse materials. + // + // This example uses two minutes. + // In production, always chose as small a value as possible + // that works for your requirements. + .withMaxAge(2, TimeUnit.MINUTES) + // Message Use Limit determines how many messages + // the caching CMM will protect with the same materials. + // + // In production, always choose as small a value as possible + // that works for your requirements. + .withMessageUseLimit(10) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final AwsCryptoResult decryptResult = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .cryptoMaterialsManager(cmm) + .ciphertext(ciphertext).build()); + final byte[] decrypted = decryptResult.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted, sourcePlaintext); + + // Verify that the encryption context used in the decrypt operation includes + // the encryption context that you specified when encrypting. + // The AWS Encryption SDK can add pairs, so don't require an exact match. + // + // In production, always use a meaningful encryption context. + encryptionContext.forEach((k, v) -> { + assert v.equals(decryptResult.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/AlgorithmSuiteEnforcement.java b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/AlgorithmSuiteEnforcement.java new file mode 100644 index 000000000..45328b15a --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/AlgorithmSuiteEnforcement.java @@ -0,0 +1,183 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.cryptomaterialsmanager.custom; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * The AWS Encryption SDK supports several different algorithm suites + * that offer different security properties. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html + *

+ * By default, the AWS Encryption SDK will let you use any of these, + * but you might want to restrict that further. + *

+ * We recommend that you use the default algorithm suite, + * which uses AES-GCM with 256-bit keys, HKDF, and ECDSA message signing. + * If your readers and writers have the same permissions, + * you might want to omit the message signature for faster operation. + * For more information about choosing a signed or unsigned algorithm suite, + * see the AWS Encryption SDK developer guide: + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html#other-algorithms + *

+ * This example shows how you can make a custom cryptographic materials manager (CMM) + * that only allows encrypt requests that either specify one of these two algorithm suites + * or do not specify an algorithm suite, in which case the default CMM uses the default algorithm suite. + */ +public class AlgorithmSuiteEnforcement { + + /** + * Indicates that an unsupported algorithm suite was requested. + */ + static class UnapprovedAlgorithmSuiteException extends RuntimeException { + UnapprovedAlgorithmSuiteException() { + super("Unapproved algorithm suite requested!"); + } + } + + /** + * Only allow encryption requests for approved algorithm suites. + */ + static class RequireApprovedAlgorithmSuitesCryptoMaterialsManager implements CryptoMaterialsManager { + + private final CryptoMaterialsManager cmm; + private final Set ALLOWED_ALGORITHM_SUITES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, // the default algorithm suite + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256))); // the recommended unsigned algorithm suite + + /** + * Set up the inner cryptographic materials manager using the provided keyring. + * + * @param keyring Keyring to use in the inner cryptographic materials manager + */ + RequireApprovedAlgorithmSuitesCryptoMaterialsManager(Keyring keyring) { + // Wrap the provided keyring in the default cryptographic materials manager (CMM). + // + // This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + // do if you provide a keyring instead of a CMM. + cmm = new DefaultCryptoMaterialsManager(keyring); + } + + /** + * Block any requests that include an unapproved algorithm suite. + */ + @Override + public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { + if (request.getRequestedAlgorithm() != null && !ALLOWED_ALGORITHM_SUITES.contains(request.getRequestedAlgorithm())) { + throw new UnapprovedAlgorithmSuiteException(); + } + + return cmm.getMaterialsForEncrypt(request); + } + + /** + * Be more permissive on decrypt and just pass through. + */ + @Override + public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { + return cmm.decryptMaterials(request); + } + } + + + /** + * Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + * + * @param awsKmsCmk The ARN of an AWS KMS CMK that protects data keys + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsCmk, final byte[] sourcePlaintext) { + // Instantiate the AWS Encryption SDK. + final AwsCrypto awsEncryptionSdk = new AwsCrypto(); + + // Prepare your encryption context. + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + final Map encryptionContext = new HashMap<>(); + encryptionContext.put("encryption", "context"); + encryptionContext.put("is not", "secret"); + encryptionContext.put("but adds", "useful metadata"); + encryptionContext.put("that can help you", "be confident that"); + encryptionContext.put("the data you are handling", "is what you think it is"); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Create the algorithm suite restricting cryptographic materials manager using your keyring. + final CryptoMaterialsManager cmm = new RequireApprovedAlgorithmSuitesCryptoMaterialsManager(keyring); + + // Demonstrate that the algorithm suite restricting CMM will not let you use an unapproved algorithm suite. + awsEncryptionSdk.setEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF); + + try { + awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + // The algorithm suite restricting CMM keeps this from happening. + throw new AssertionError("The algorithm suite restricting CMM does not let this happen!"); + } catch (UnapprovedAlgorithmSuiteException ex) { + // You asked for an unapproved algorithm suite. + // Reaching this point means everything is working as expected. + } + + // Set an approved algorithm suite. + awsEncryptionSdk.setEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final AwsCryptoResult decryptResult = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .cryptoMaterialsManager(cmm) + .ciphertext(ciphertext).build()); + final byte[] decrypted = decryptResult.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted, sourcePlaintext); + + // Verify that the encryption context used in the decrypt operation includes + // the encryption context that you specified when encrypting. + // The AWS Encryption SDK can add pairs, so don't require an exact match. + // + // In production, always use a meaningful encryption context. + encryptionContext.forEach((k, v) -> { + assert v.equals(decryptResult.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/RequiringEncryptionContextFields.java b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/RequiringEncryptionContextFields.java new file mode 100644 index 000000000..c03a2f550 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/cryptomaterialsmanager/custom/RequiringEncryptionContextFields.java @@ -0,0 +1,197 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.cryptomaterialsmanager.custom; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Encryption context is a powerful tool for access and audit controls + * because it lets you tie *non-secret* metadata about a plaintext value to the encrypted message. + * Within the AWS Encryption SDK, + * you can use cryptographic materials managers to analyse the encryption context + * to provide logical controls and additional metadata. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + *

+ * If you are using the AWS Encryption SDK with AWS KMS, + * you can use AWS KMS to provide additional powerful controls using the encryption context. + * For more information on that, see the KMS developer guide: + *

+ * https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context + *

+ * This example shows how to create a custom cryptographic materials manager (CMM) + * that requires a particular field in the encryption context. + */ +public class RequiringEncryptionContextFields { + + /** + * Indicates that an encryption context was found that lacked a classification identifier. + */ + static class MissingClassificationException extends RuntimeException { + MissingClassificationException() { + super("Encryption context does not contain classification!"); + } + } + + /** + * Only allow requests when the encryption context contains a classification identifier. + */ + static class ClassificationRequiringCryptoMaterialsManager implements CryptoMaterialsManager { + + private final CryptoMaterialsManager cmm; + private static final String CLASSIFICATION_KEY = "classification"; + + ClassificationRequiringCryptoMaterialsManager(Keyring keyring) { + // Wrap the provided keyring in the default cryptographic materials manager (CMM). + // + // This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + // do if you provide a keyring instead of a CMM. + cmm = new DefaultCryptoMaterialsManager(keyring); + } + + /** + * Block any requests that do not contain a classification identifier in the encryption context. + */ + @Override + public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { + if (!request.getContext().containsKey(CLASSIFICATION_KEY)) { + throw new MissingClassificationException(); + } + + return cmm.getMaterialsForEncrypt(request); + } + + /** + * Block any requests that do not contain a classification identifier in the encryption context. + */ + @Override + public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { + if (!request.getEncryptionContext().containsKey(CLASSIFICATION_KEY)) { + throw new MissingClassificationException(); + } + + return cmm.decryptMaterials(request); + } + } + + + /** + * Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + * + * @param awsKmsCmk The ARN of an AWS KMS CMK that protects data keys + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsCmk, final byte[] sourcePlaintext) { + // Instantiate the AWS Encryption SDK. + final AwsCrypto awsEncryptionSdk = new AwsCrypto(); + + // Prepare your encryption context. + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + final Map encryptionContext = new HashMap<>(); + encryptionContext.put("encryption", "context"); + encryptionContext.put("is not", "secret"); + encryptionContext.put("but adds", "useful metadata"); + encryptionContext.put("that can help you", "be confident that"); + encryptionContext.put("the data you are handling", "is what you think it is"); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Create the classification requiring cryptographic materials manager using your keyring. + final CryptoMaterialsManager cmm = new ClassificationRequiringCryptoMaterialsManager(keyring); + + // Demonstrate that the classification requiring CMM will not let you encrypt without a classification identifier. + try { + awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + // The classification requiring CMM keeps this from happening. + throw new AssertionError("The classification requiring CMM does not let this happen!"); + } catch (MissingClassificationException ex) { + // Your encryption context did not contain a classification identifier. + // Reaching this point means everything is working as expected. + } + + // Create an encryption context with the required classification key. + final Map classifiedEncryptionContext = new HashMap<>(encryptionContext); + classifiedEncryptionContext.put("classification", "secret"); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .encryptionContext(classifiedEncryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final AwsCryptoResult decryptResult = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .cryptoMaterialsManager(cmm) + .ciphertext(ciphertext).build()); + final byte[] decrypted = decryptResult.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted, sourcePlaintext); + + // Verify that the encryption context used in the decrypt operation includes + // the encryption context that you specified when encrypting. + // The AWS Encryption SDK can add pairs, so don't require an exact match. + // + // In production, always use a meaningful encryption context. + encryptionContext.forEach((k, v) -> { + assert v.equals(decryptResult.getEncryptionContext().get(k)); + }); + + // Now demonstrate the decrypt path of the classification requiring cryptographic materials manager. + + // Encrypt your plaintext using the keyring and do not include a classification identifier. + final AwsCryptoResult unclassifiedEncryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] unclassifiedCiphertext = unclassifiedEncryptResult.getResult(); + + assert !unclassifiedEncryptResult.getEncryptionContext().containsKey("classification"); + + // Demonstrate that the classification requiring CMM + // will not let you decrypt messages without classification identifiers. + try { + awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .cryptoMaterialsManager(cmm) + .ciphertext(unclassifiedCiphertext).build()); + // The classification requiring CMM keeps this from happening. + throw new AssertionError("The classification requiring CMM does not let this happen!"); + } catch (MissingClassificationException ex) { + // Your encryption context did not contain a classification identifier. + // Reaching this point means everything is working as expected. + } + } +}