diff --git a/README.md b/README.md index 0387070cc..24858613d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # AWS Encryption SDK for Java -The AWS Encryption SDK enables secure client-side encryption. It uses cryptography best practices to protect your data and the encryption keys used to protect that data. Each data object is protected with a unique data encryption key (DEK), and the DEK is protected with a key encryption key (KEK) called a *master key*. The encrypted DEK is combined with the encrypted data into a single [encrypted message](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html), so you don't need to keep track of the DEKs for your data. The SDK supports master keys in [AWS Key Management Service](https://aws.amazon.com/kms/) (KMS), and it also provides APIs to define and use other master key providers. The SDK provides methods for encrypting and decrypting strings, byte arrays, and byte streams. For details, see the [example code][examples] and the [Javadoc](https://aws.github.io/aws-encryption-sdk-java/javadoc/). +The AWS Encryption SDK is a client-side encryption library designed to make it easy for everyone to encrypt and decrypt data using industry standards and best practices. It enables you to focus on the core functionality of your application, rather than on how to best encrypt and decrypt your data. -For more details about the design and architecture of the SDK, see the [official documentation](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/). +For details about the design, architecture and usage of the SDK, see the [official documentation](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/), [example code][examples] and the [Javadoc](https://aws.github.io/aws-encryption-sdk-java/javadoc/). ## Getting Started @@ -58,75 +58,9 @@ You can get the latest release from Maven: ``` -### Get Started - -The following code sample demonstrates how to get started: - -1. Instantiate the SDK. -2. Define the master key provider. -3. Encrypt and decrypt data. - -```java -// This sample code encrypts and then decrypts a string using a KMS CMK. -// You provide the KMS key ARN and plaintext string as arguments. -package com.amazonaws.crypto.examples; - -import java.util.Collections; -import java.util.Map; - -import com.amazonaws.encryptionsdk.AwsCrypto; -import com.amazonaws.encryptionsdk.CryptoResult; -import com.amazonaws.encryptionsdk.kms.KmsMasterKey; -import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; - -public class StringExample { - private static String keyArn; - private static String data; - - public static void main(final String[] args) { - keyArn = args[0]; - data = args[1]; - - // Instantiate the SDK - final AwsCrypto crypto = new AwsCrypto(); - - // Set up the master key provider - final KmsMasterKeyProvider prov = new KmsMasterKeyProvider(keyArn); - - // Encrypt the data - // - // NOTE: Encrypted data should have associated encryption context - // to protect integrity. For this example, just use a placeholder - // value. For more information about encryption context, see - // https://amzn.to/1nSbe9X (blogs.aws.amazon.com) - final Map context = Collections.singletonMap("Example", "String"); - - final String ciphertext = crypto.encryptString(prov, data, context).getResult(); - System.out.println("Ciphertext: " + ciphertext); - - // Decrypt the data - final CryptoResult decryptResult = crypto.decryptString(prov, ciphertext); - // Check the encryption context (and ideally the master key) to - // ensure this is the expected ciphertext - if (!decryptResult.getMasterKeyIds().get(0).equals(keyArn)) { - throw new IllegalStateException("Wrong key id!"); - } - - // The SDK may add information to the encryption context, so check to - // ensure all of the values are present - for (final Map.Entry e : context.entrySet()) { - if (!e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey()))) { - throw new IllegalStateException("Wrong Encryption Context!"); - } - } - - // The data is correct, so output it. - System.out.println("Decrypted: " + decryptResult.getResult()); - } -} -``` +### Sample Code -You can find more examples in the [examples directory][examples]. +You can find sample code in the [examples directory][examples]. ## Public API diff --git a/pom.xml b/pom.xml index 6c485750c..22b369c1e 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ com.amazonaws aws-java-sdk - 1.11.561 + 1.11.677 true @@ -53,16 +53,23 @@ - org.mockito - mockito-core - 2.28.1 + org.junit.jupiter + junit-jupiter + 5.5.2 + test + + + + org.junit.vintage + junit-vintage-engine + 5.5.2 test - junit - junit - 4.12 + org.mockito + mockito-junit-jupiter + 3.1.0 test @@ -73,6 +80,19 @@ test + + com.amazonaws + aws-lambda-java-core + 1.2.0 + test + + + + com.amazonaws + aws-lambda-java-events + 2.2.7 + test + com.google.code.findbugs @@ -190,7 +210,7 @@ - full-test-suite + test-suite true @@ -201,20 +221,17 @@ maven-surefire-plugin 2.22.0 - - **/AllTestsSuite.java - + ad_hoc + fast-tests-only - - false - @@ -222,9 +239,32 @@ maven-surefire-plugin 2.22.0 - - **/FastTestsOnlySuite.java - + ad_hoc, integration + + true + + + 120 + + + + + + + + + integration + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + integration diff --git a/src/examples/README.md b/src/examples/README.md new file mode 100644 index 000000000..c8ae480b7 --- /dev/null +++ b/src/examples/README.md @@ -0,0 +1,126 @@ +# AWS Encryption SDK Examples + +This section features examples that show you +how to use the AWS Encryption SDK. +We demonstrate how to use the encryption and decryption APIs +and how to set up some common configuration patterns. + +## APIs + +The AWS Encryption SDK provides two high-level APIs: +one-step APIs that process the entire operation in memory +and streaming APIs. + +You can find examples that demonstrate these APIs +in the [`examples`](./java/com/amazonaws/crypto/examples) directory. + +## Configuration + +To use the encryption and decryption APIs, +you need to describe how you want the library to protect your data keys. +You can do this by configuring +[keyrings](#keyrings) or [cryptographic materials managers](#cryptographic-materials-managers), +or by configuring [master key providers](#master-key-providers). +These examples will show you how to use the configuration tools that we include for you +and how to create some of your own. +We start with AWS KMS examples, then show how to use other wrapping keys. + +* Using AWS Key Management Service (AWS KMS) + * How to use one AWS KMS CMK + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/awskms/SingleCmk.java) + * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/SingleCmk.java) + * How to use multiple AWS KMS CMKs in different regions + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/awskms/MultipleRegions.java) + * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/MultipleRegions.java) + * How to decrypt when you don't know the CMK + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecrypt.java) + * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/DiscoveryDecrypt.java) + * How to decrypt within a region + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptInRegionOnly.java) + * How to decrypt with a preferred region but failover to others + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptWithPreferredRegions.java) +* Using raw wrapping keys + * How to use a raw AES wrapping key + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/rawaes/RawAes.java) + * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/rawaes/RawAes.java) + * How to use a raw RSA wrapping key + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsa.java) + * [with master key providers](./java/com/amazonaws/crypto/examples/masterkeyprovider/rawrsa/RawRsa.java) + * How to encrypt with a raw RSA public key wrapping key without access to the private key + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/rawrsa/PublicPrivateKeySeparate.java) + * How to use a raw RSA wrapping key when the key is DER encoded + * [with keyrings](./java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsaDerEncoded.java) +* Combining 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 + +Keyrings are the most common way for you to configure the AWS Encryption SDK. +They determine how the AWS Encryption SDK protects your data. +You can find these examples in ['examples/keyring`](./java/com/amazonaws/crypto/examples/keyring). + +### Cryptographic Materials Managers + +Keyrings define how your data keys are protected, +but there is more going on here than just protecting data keys. + +Cryptographic materials managers give you higher-level controls +over how the AWS Encryption SDK protects your data. +This can include things like +enforcing the use of certain algorithm suites or encryption context settings, +reusing data keys across messages, +or changing how you interact with keyrings. +You can find these examples in +[`examples/crypto_materials_manager`](./java/com/amazonaws/crypto/examples/cryptomaterialsmanager). + +### Master Key Providers + +Before there were keyrings, there were master key providers. +Master key providers were the original configuration structure +that we provided for defining how you want to protect your data keys. +Keyrings provide a simpler experience and often more powerful configuration options, +but if you need to use master key providers, +need help migrating from master key providers to keyrings, +or simply want to see the difference between these configuration experiences, +you can find these examples in [`examples/masterkeyprovider`](./java/com/amazonaws/crypto/examples/masterkeyprovider). + +## Legacy + +This section includes older examples, +including examples of using master keys and master key providers. +You can use them as a reference, +but we recommend looking at the newer examples, which explain the preferred ways of using this library. +You can find these examples in [`examples/legacy`](./java/com/amazonaws/crypto/examples/legacy). + +# Writing Examples + +If you want to contribute a new example, that's awesome! +To make sure that your example is tested in our CI, +please make sure that it meets the following requirements: + +1. The example MUST be a distinct class in the [`examples`](./java/com/amazonaws/crypto/examples) directory. +1. Each example file MUST contain exactly one example. +1. Each example file MUST contain a static method called `run` that runs the example. +1. If your `run` method needs any of the following inputs, + the parameters MUST have the following types: + * `com.amazonaws.encryptionsdk.kms.AwsKmsCmkId` : A single AWS KMS CMK ARN. + * NOTE: You can assume that automatically discovered credentials have + `kms:GenerateDataKey`, `kms:Encrypt`, and `kms:Decrypt` permissions on this CMK. + * `List` : + A list of AWS KMS CMK ARNs to use for encrypting and decrypting data keys. + * NOTE: You can assume that automatically discovered credentials have + `kms:Encrypt` and `kms:Decrypt` permissions on these CMKs. + * `byte[]` : Plaintext data to encrypt. + * `java.io.File` : A path to a file containing plaintext to encrypt. + * NOTE: You can assume that you have write access to the parent directory + and that anything you do in that directory will be cleaned up + by our test runners. +1. Any additional parameters MUST be optional and nullable and not of the same type as the above parameters. diff --git a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingDefaults.java b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingDefaults.java new file mode 100644 index 000000000..ae9b97e5f --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingDefaults.java @@ -0,0 +1,135 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoInputStream; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CreateDecryptingInputStreamRequest; +import com.amazonaws.encryptionsdk.CreateEncryptingInputStreamRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.util.IOUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * This example shows how to use the streaming encrypt and decrypt APIs when working with files. + *

+ * One benefit of using the streaming API is that + * we can check the encryption context before we start decrypting. + *

+ * 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. + */ +public class FileStreamingDefaults { + + /** + * Demonstrate an encrypt/decrypt cycle using the streaming encrypt/decrypt APIs with files. + * + * @param awsKmsCmk The ARN of an AWS KMS CMK that protects data keys + * @param sourcePlaintextFile Plaintext file to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsCmk, final File sourcePlaintextFile) throws IOException { + // Instantiate the AWS Encryption SDK. + final AwsCrypto awsEncryptionSdk = new AwsCrypto(); + + // We assume that you can also write to the directory containing the plaintext file, + // so that is where we will put all of the results. + final File encryptedFile = new File(sourcePlaintextFile.getPath() + ".encrypted"); + final File decryptedFile = new File(sourcePlaintextFile.getPath() + ".decrypted"); + encryptedFile.deleteOnExit(); + decryptedFile.deleteOnExit(); + + // 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 encrypting input stream with the keyring and encryption context. + // Because the file might be too large to load into memory, + // we stream the data, instead of loading it all at once. + try (final AwsCryptoInputStream encryptingStream = awsEncryptionSdk.createEncryptingInputStream( + CreateEncryptingInputStreamRequest.builder() + .keyring(keyring) + .encryptionContext(encryptionContext) + .inputStream(new FileInputStream(sourcePlaintextFile)).build())) { + + // Encrypt the data and write the ciphertext to the encrypted file. + try (FileOutputStream out = new FileOutputStream(encryptedFile)) { + IOUtils.copy(encryptingStream, out); + } + } + + // Demonstrate that the ciphertext and plaintext are different. + assert !compareFiles(sourcePlaintextFile, encryptedFile); + + // Create the decrypting input stream with the keyring. + try (final AwsCryptoInputStream decryptingStream = awsEncryptionSdk.createDecryptingInputStream( + CreateDecryptingInputStreamRequest.builder() + .keyring(keyring) + .inputStream(new FileInputStream(encryptedFile)).build())) { + + // Check the encryption context before we start decrypting. + // + // 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. + final AwsCryptoResult decryptResult = decryptingStream.getAwsCryptoResult(); + encryptionContext.forEach((k, v) -> { + assert v.equals(decryptResult.getEncryptionContext().get(k)); + }); + + // Now that we are more confident that we will decrypt the right message, + // we can start decrypting. + try (FileOutputStream out = new FileOutputStream(decryptedFile)) { + IOUtils.copy(decryptingStream, out); + } + } + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert compareFiles(sourcePlaintextFile, decryptedFile); + } + + private static boolean compareFiles(File file1, File file2) throws IOException { + if (file1.length() != file2.length()) { + return false; + } + + try (BufferedReader file1Reader = Files.newBufferedReader(file1.toPath()); + BufferedReader file2Reader = Files.newBufferedReader(file2.toPath())) { + String file1Line; + String file2Line; + + while ((file1Line = file1Reader.readLine()) != null && + (file2Line = file2Reader.readLine()) != null) { + if (!Objects.equals(file1Line, file2Line)) { + return false; + } + } + + return true; + } + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/InMemoryStreamingDefaults.java b/src/examples/java/com/amazonaws/crypto/examples/InMemoryStreamingDefaults.java new file mode 100644 index 000000000..94586f7a6 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/InMemoryStreamingDefaults.java @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoInputStream; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CreateDecryptingInputStreamRequest; +import com.amazonaws.encryptionsdk.CreateEncryptingInputStreamRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.util.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example shows how to use the streaming encrypt and decrypt APIs on data in memory. + *

+ * One benefit of using the streaming API is that + * we can check the encryption context before we start decrypting. + *

+ * 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. + */ +public class InMemoryStreamingDefaults { + + /** + * Demonstrate an encrypt/decrypt cycle using the streaming encrypt/decrypt APIs in-memory. + * + * @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) throws IOException { + // 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); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(sourcePlaintext); + + // Create the encrypting input stream with the keyring and encryption context. + final AwsCryptoInputStream encryptingStream = awsEncryptionSdk.createEncryptingInputStream( + CreateEncryptingInputStreamRequest.builder() + .keyring(keyring) + .encryptionContext(encryptionContext) + .inputStream(inputStream).build()); + + // Encrypt the plaintext and write the results into the ciphertext. + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + IOUtils.copy(encryptingStream, ciphertext); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext.toByteArray(), sourcePlaintext); + + // Decrypt your encrypted data using the same keyring 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 AwsCryptoInputStream decryptingStream = awsEncryptionSdk.createDecryptingInputStream( + CreateDecryptingInputStreamRequest.builder() + .keyring(keyring) + .inputStream(new ByteArrayInputStream(ciphertext.toByteArray())).build()); + + // Check the encryption context before we start decrypting. + // + // 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. + final AwsCryptoResult decryptResult = decryptingStream.getAwsCryptoResult(); + encryptionContext.forEach((k, v) -> { + assert v.equals(decryptResult.getEncryptionContext().get(k)); + }); + + // Now that we are more confident that we will decrypt the right message, + // we can start decrypting. + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + IOUtils.copy(decryptingStream, decrypted); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted.toByteArray(), sourcePlaintext); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/OneStepDefaults.java b/src/examples/java/com/amazonaws/crypto/examples/OneStepDefaults.java new file mode 100644 index 000000000..5a9c4bc13 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/OneStepDefaults.java @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +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; + +/** + * This example shows how to use the one-step encrypt and decrypt APIs. + *

+ * 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. + */ +public class OneStepDefaults { + + /** + * Demonstrate an encrypt/decrypt cycle using the one-step encrypt/decrypt APIs. + * + * @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); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/OneStepUnsigned.java b/src/examples/java/com/amazonaws/crypto/examples/OneStepUnsigned.java new file mode 100644 index 000000000..5d457d2f3 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/OneStepUnsigned.java @@ -0,0 +1,97 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +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; + +/** + * This example shows how to specify an algorithm suite + * when using the one-step encrypt and decrypt APIs. + *

+ * 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. + *

+ * The default algorithm suite includes a message-level signature + * that protects you from an attacker who has *decrypt* but not *encrypt* capability + * for a wrapping key that you used when encrypting a message + * under multiple wrapping keys. + *

+ * However, if all of your readers and writers have the same permissions, + * then this additional protection does not always add value. + * This example shows you how to select another algorithm suite + * that has all of the other properties of the default suite + * but does not include a message-level signature. + */ +public class OneStepUnsigned { + + /** + * Demonstrate requesting a specific algorithm suite through the one-step encrypt/decrypt APIs. + * + * @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 and specify the algorithm suite that we want to use. + final AwsCrypto awsEncryptionSdk = new AwsCrypto(); + awsEncryptionSdk.setEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256); + + // 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); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/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. + } + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/CustomClientSupplier.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/CustomClientSupplier.java new file mode 100644 index 000000000..0e6ddcdf5 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/CustomClientSupplier.java @@ -0,0 +1,139 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.auth.profile.ProfileCredentialsProvider; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsClientSupplier; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; +import com.amazonaws.services.kms.AWSKMS; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * By default, the KMS keyring uses a client supplier that + * supplies a client with the same configuration for every region. + * If you need different behavior, you can write your own client supplier. + *

+ * You might use this + * if you need different credentials in different AWS regions. + * This might be because you are crossing partitions (ex: "aws" and "aws-cn") + * or if you are working with regions that have separate authentication silos + * like "ap-east-1" and "me-south-1". + *

+ * This example shows how to create a client supplier + * that will supply KMS clients with valid credentials for the target region + * even when working with regions that need different credentials. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For another example of how to use the KMS keyring with a custom client configuration, + * see the {@link CustomKmsClientConfig} example. + *

+ * For examples of how to use the KMS Discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, {@link DiscoveryDecryptInRegionOnly}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class CustomClientSupplier { + + static class MultiPartitionClientSupplier implements AwsKmsClientSupplier { + + private final AwsKmsClientSupplier chinaSupplier = StandardAwsKmsClientSuppliers.defaultBuilder() + .credentialsProvider(new ProfileCredentialsProvider("china")).build(); + private final AwsKmsClientSupplier middleEastSupplier = StandardAwsKmsClientSuppliers.defaultBuilder() + .credentialsProvider(new ProfileCredentialsProvider("middle-east")).build(); + private final AwsKmsClientSupplier hongKongSupplier = StandardAwsKmsClientSuppliers.defaultBuilder() + .credentialsProvider(new ProfileCredentialsProvider("hong-kong")).build(); + private final AwsKmsClientSupplier defaultSupplier = StandardAwsKmsClientSuppliers.defaultBuilder().build(); + + /** + * Returns a client for the requested region. + * + * @param regionId The AWS region + * @return The AWSKMS client + */ + @Override + public AWSKMS getClient(String regionId) { + if (regionId.startsWith("cn-")) { + return chinaSupplier.getClient(regionId); + } else if (regionId.startsWith("me-")) { + return middleEastSupplier.getClient(regionId); + } else if (regionId.equals("ap-east-1")) { + return hongKongSupplier.getClient(regionId); + } else { + return defaultSupplier.getClient(regionId); + } + } + } + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS keyring with a custom client supplier. + * + * @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.awsKmsBuilder() + .generatorKeyId(awsKmsCmk) + .awsKmsClientSupplier(new MultiPartitionClientSupplier()) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/keyring/awskms/CustomKmsClientConfig.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/CustomKmsClientConfig.java new file mode 100644 index 000000000..fd3af280d --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/CustomKmsClientConfig.java @@ -0,0 +1,118 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsClientSupplier; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * By default, the KMS keyring uses the default configurations + * for all KMS clients and uses the default discoverable credentials. + * If you need to change this configuration, + * you can configure the client supplier. + *

+ * This example shows how to use custom-configured clients with the KMS keyring. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For another example of how to use the KMS keyring with a custom client configuration, + * see the {@link CustomKmsClientConfig} example. + *

+ * For examples of how to use the KMS Discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, {@link DiscoveryDecryptInRegionOnly}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class CustomKmsClientConfig { + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS keyring with custom KMS client configuration. + * + * @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"); + + // Prepare your custom configuration values. + // + // Set your custom connection timeout value. + // https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html + final ClientConfiguration clientConfiguration = new ClientConfiguration() + .withConnectionTimeout(10000) // 10,000 milliseconds + .withUserAgentSuffix(VersionInfo.USER_AGENT); + + // Use your custom configuration values to configure your client supplier. + // For this example we will just use the default credentials provider + // but if you need to, you can set a custom credentials provider as well. + final AwsKmsClientSupplier clientSupplier = StandardAwsKmsClientSuppliers.defaultBuilder() + .clientConfiguration(clientConfiguration) + .build(); + + // Create the keyring that determines how your data keys are protected, + // providing the client supplier that you created. + final Keyring keyring = StandardKeyrings.awsKmsBuilder() + .generatorKeyId(awsKmsCmk) + .awsKmsClientSupplier(clientSupplier) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/keyring/awskms/DiscoveryDecrypt.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecrypt.java new file mode 100644 index 000000000..64b031887 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecrypt.java @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +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; + +/** + * When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. + * This is true both on encrypt and on decrypt. + * However, sometimes you need more flexibility on decrypt, + * especially when you don't know which CMKs were used to encrypt a message. + * To address this need, you can use a KMS discovery keyring. + * The KMS discovery keyring does nothing on encrypt + * but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + *

+ * This example shows how to configure and use a KMS discovery keyring. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For examples of how to use the KMS keyring with custom client configurations, + * see the {@link CustomClientSupplier} + * and {@link CustomKmsClientConfig} examples. + *

+ * For examples of how to use the KMS discovery keyring on decrypt, + * see the {@link DiscoveryDecryptInRegionOnly}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class DiscoveryDecrypt { + + /** + * Demonstrate configuring a KMS discovery keyring for decryption. + * + * @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 encryptKeyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Create a KMS discovery keyring to use on decrypt. + final Keyring decryptKeyring = StandardKeyrings.awsKmsDiscoveryBuilder().build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(encryptKeyring) + .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 KMS discovery keyring. + // + // 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() + .keyring(decryptKeyring) + .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/keyring/awskms/DiscoveryDecryptInRegionOnly.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptInRegionOnly.java new file mode 100644 index 000000000..ad545cbec --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptInRegionOnly.java @@ -0,0 +1,118 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.arn.Arn; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +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.kms.StandardAwsKmsClientSuppliers; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.singleton; + +/** + * When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. + * This is true both on encrypt and on decrypt. + * However, sometimes you need more flexibility on decrypt, + * especially if you don't know which CMK was used to encrypt a message. + * To address this need, you can use a KMS discovery keyring. + * The KMS discovery keyring does nothing on encrypt + * but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + *

+ * However, sometimes you need to be a *bit* more restrictive than that. + * To address this need, you can use a client supplier that restricts the regions a KMS keyring can talk to. + *

+ * This example shows how to configure and use a KMS regional discovery keyring that is restricted to one region. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For examples of how to use the KMS keyring with custom client configurations, + * see the {@link CustomClientSupplier} + * and {@link CustomKmsClientConfig} examples. + *

+ * For examples of how to use the KMS discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class DiscoveryDecryptInRegionOnly { + + /** + * Demonstrate configuring a KMS keyring to only work within a single region. + * + * @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 encryptKeyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Extract the region from the CMK ARN. + final String decryptRegion = Arn.fromString(awsKmsCmk.toString()).getRegion(); + + // Create the KMS discovery keyring that we will use on decrypt. + // + // The client supplier that we specify here will only supply clients for the specified region. + // The keyring only attempts to decrypt data keys if it can get a client for that region, + // so this keyring will now ignore any data keys that were encrypted under a CMK in another region. + final Keyring decryptKeyring = StandardKeyrings.awsKmsDiscoveryBuilder() + .awsKmsClientSupplier(StandardAwsKmsClientSuppliers.allowRegionsBuilder(singleton(decryptRegion)).build()) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(encryptKeyring) + .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 KMS discovery keyring. + // + // 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() + .keyring(decryptKeyring) + .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/keyring/awskms/DiscoveryDecryptWithPreferredRegions.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptWithPreferredRegions.java new file mode 100644 index 000000000..5068395d2 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/DiscoveryDecryptWithPreferredRegions.java @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +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.kms.StandardAwsKmsClientSuppliers; +import com.amazonaws.services.kms.AWSKMSClientBuilder; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.singleton; + +/** + * When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. + * This is true both on encrypt and on decrypt. + * However, sometimes you need more flexibility on decrypt, + * especially if you might not know beforehand which CMK was used to encrypt a message. + * To address this need, you can use a KMS discovery keyring. + * The KMS discovery keyring will do nothing on encrypt + * but will attempt to decrypt *any* data keys that were encrypted under a KMS CMK. + *

+ * However, sometimes you need to be a *bit* more restrictive than that. + * To address this need, you can use a client supplier to restrict what regions a KMS keyring can talk to. + *

+ * A more complex but more common use-case is that you would *prefer* to stay within a region, + * but you would rather make calls to other regions than fail to decrypt the message. + * In this case, you want a keyring that will try to decrypt data keys in this region first, + * then try other regions. + *

+ * This example shows how to configure and use a multi-keyring with the KMS keyring + * to prefer the current AWS region while also failing over to other AWS regions. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For examples of how to use the KMS keyring with custom client configurations, + * see the {@link CustomClientSupplier} + * and {@link CustomKmsClientConfig} examples. + *

+ * For examples of how to use the KMS discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, + * and {@link DiscoveryDecryptInRegionOnly} examples. + */ +public class DiscoveryDecryptWithPreferredRegions { + + /** + * Demonstrate configuring a keyring preferring a particular AWS region and failing over to others. + * + * @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 encryptKeyring = StandardKeyrings.awsKms(awsKmsCmk); + + // To create our decrypt keyring, we need to know our current default AWS region. + final String localRegion = AWSKMSClientBuilder.standard().getRegion(); + + // Now, use that region name to create two KMS discovery keyrings: + // + // One that only works in the local region + final Keyring localRegionDecryptKeyring = StandardKeyrings.awsKmsDiscoveryBuilder() + .awsKmsClientSupplier(StandardAwsKmsClientSuppliers.allowRegionsBuilder(singleton(localRegion)).build()) + .build(); + // and one that will work in any other region but NOT the local region. + final Keyring otherRegionsDecryptKeyring = StandardKeyrings.awsKmsDiscoveryBuilder() + .awsKmsClientSupplier(StandardAwsKmsClientSuppliers.denyRegionsBuilder(singleton(localRegion)).build()) + .build(); + + // Finally, combine those two keyrings into a multi-keyring. + // + // The multi-keyring steps through its member keyrings in the order that you provide them, + // attempting to decrypt every encrypted data key with each keyring before moving on to the next keyring. + // Because of this, otherRegionsDecryptKeyring will not be called + // unless localRegionDecryptKeyring fails to decrypt every encrypted data key. + final Keyring decryptKeyring = StandardKeyrings.multi(localRegionDecryptKeyring, otherRegionsDecryptKeyring); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(encryptKeyring) + .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 multi-keyring. + // + // 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() + .keyring(decryptKeyring) + .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/keyring/awskms/MultipleRegions.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/MultipleRegions.java new file mode 100644 index 000000000..8eabf06c6 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/MultipleRegions.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.keyring.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +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.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This example shows how to configure and use a KMS keyring with CMKs in multiple regions. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with a single CMK, + * see the {@link SingleCmk} example. + *

+ * For examples of how to use the KMS keyring with custom client configurations, + * see the {@link CustomClientSupplier} + * and {@link CustomKmsClientConfig} examples. + *

+ * For examples of how to use the KMS Discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, + * {@link DiscoveryDecryptInRegionOnly}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class MultipleRegions { + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS keyring with CMKs in multiple regions. + * + * @param awsKmsGeneratorCmk The ARN of an AWS KMS CMK that protects data keys + * @param awsKmsAdditionalCmks Additional ARNs of secondary KMS CMKs + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsGeneratorCmk, final List awsKmsAdditionalCmks, 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 will encrypt your data keys under all requested CMKs. + final Keyring manyCmksKeyring = StandardKeyrings.awsKmsBuilder() + .generatorKeyId(awsKmsGeneratorCmk) + .keyIds(awsKmsAdditionalCmks) + .build(); + + // Create keyrings that each only use one of the CMKs. + // We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. + // + // We provide these in "keyIds" rather than "generatorKeyId" + // so that these keyrings cannot be used to generate a new data key. + // We will only be using them on decrypt. + final Keyring singleCmkKeyringThatGenerated = StandardKeyrings.awsKmsBuilder() + .keyIds(Collections.singletonList(awsKmsGeneratorCmk)) + .build(); + final Keyring singleCmkKeyringThatEncrypted = StandardKeyrings.awsKmsBuilder() + .keyIds(Collections.singletonList(awsKmsAdditionalCmks.get(0))) + .build(); + + // Encrypt your plaintext data using the keyring that uses all requests CMKs. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(manyCmksKeyring) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Verify that the header contains the expected number of encrypted data keys (EDKs). + // It should contain one EDK for each CMK. + assert encryptResult.getHeaders().getEncryptedKeyBlobCount() == awsKmsAdditionalCmks.size() + 1; + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data separately using the single-CMK keyrings. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final AwsCryptoResult decryptResult1 = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .keyring(singleCmkKeyringThatGenerated) + .ciphertext(ciphertext).build()); + final byte[] decrypted1 = decryptResult1.getResult(); + final AwsCryptoResult decryptResult2 = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .keyring(singleCmkKeyringThatEncrypted) + .ciphertext(ciphertext).build()); + final byte[] decrypted2 = decryptResult2.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted1, sourcePlaintext); + assert Arrays.equals(decrypted2, 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(decryptResult1.getEncryptionContext().get(k)); + assert v.equals(decryptResult2.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/SingleCmk.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/SingleCmk.java new file mode 100644 index 000000000..b77824e83 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/awskms/SingleCmk.java @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +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; + +/** + * This example shows how to configure and use a KMS keyring with a single KMS CMK. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + *

+ * For an example of how to use the KMS keyring with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

+ * For examples of how to use the KMS keyring with custom client configurations, + * see the {@link CustomClientSupplier} + * and {@link CustomKmsClientConfig} examples. + *

+ * For examples of how to use the KMS Discovery keyring on decrypt, + * see the {@link DiscoveryDecrypt}, + * {@link DiscoveryDecryptInRegionOnly}, + * and {@link DiscoveryDecryptWithPreferredRegions} examples. + */ +public class SingleCmk { + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS keyring with a single CMK. + * + * @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); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/keyring/multi/AwsKmsWithEscrow.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/multi/AwsKmsWithEscrow.java new file mode 100644 index 000000000..541c017cb --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/multi/AwsKmsWithEscrow.java @@ -0,0 +1,149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.multi; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * One use-case that we have seen customers need is + * the ability to enjoy the benefits of AWS KMS during normal operation + * but retain the ability to decrypt encrypted messages without access to AWS KMS. + * This example shows how you can use the multi-keyring to achieve this + * by combining a KMS keyring with a raw RSA keyring. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-multi-keyring + *

+ * For more examples of how to use the KMS keyring, see the keyring/awskms examples. + *

+ * For more examples of how to use the raw RSA keyring, see the keyring/rawrsa examples. + *

+ * In this example we generate a RSA keypair + * but in practice you would want to keep your private key in an HSM + * or other key management system. + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class AwsKmsWithEscrow { + + /** + * Demonstrate configuring a keyring to use an AWS KMS CMK and a RSA wrapping key. + * + * @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) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your keyring. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Create the encrypt keyring that only has access to the public key. + final Keyring escrowEncryptKeyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name are defined by you + // and are used by the raw RSA keyring + // to determine whether it should attempt to decrypt + // an encrypted data key. + // + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .publicKey(keyPair.getPublic()) + // The padding scheme tells the raw RSA keyring + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + .paddingScheme(RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Create the decrypt keyring that has access to the private key. + final Keyring escrowDecryptKeyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name MUST match the encrypt keyring. + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .privateKey(keyPair.getPrivate()) + // The padding scheme MUST match the encrypt keyring. + .paddingScheme(RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Create the KMS keyring that you will use from decryption during normal operations. + final Keyring kmsKeyring = StandardKeyrings.awsKms(awsKmsCmk); + + // Combine the KMS keyring and the escrow encrypt keyring using the multi-keyring. + final Keyring encryptKeyring = StandardKeyrings.multi(kmsKeyring, escrowEncryptKeyring); + + // Encrypt your plaintext data using the multi-keyring. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(encryptKeyring) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Verify that the header contains the expected number of encrypted data keys (EDKs). + // It should contain one EDK for KMS and one for the escrow key. + assert encryptResult.getHeaders().getEncryptedKeyBlobCount() == 2; + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data separately using the KMS keyring and the escrow decrypt keyring. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final AwsCryptoResult decryptedKmsResult = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .keyring(kmsKeyring) + .ciphertext(ciphertext).build()); + final byte[] decryptedKms = decryptedKmsResult.getResult(); + final AwsCryptoResult decryptedEscrowResult = awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .keyring(escrowDecryptKeyring) + .ciphertext(ciphertext).build()); + final byte[] decryptedEscrow = decryptedKmsResult.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decryptedKms, sourcePlaintext); + assert Arrays.equals(decryptedEscrow, 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(decryptedKmsResult.getEncryptionContext().get(k)); + assert v.equals(decryptedEscrowResult.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/keyring/rawaes/RawAes.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawaes/RawAes.java new file mode 100644 index 000000000..9d79f7440 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawaes/RawAes.java @@ -0,0 +1,101 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.rawaes; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This examples shows how to configure and use a raw AES keyring. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class RawAes { + + /** + * Demonstrate an encrypt/decrypt cycle using a raw AES keyring. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(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"); + + // Generate an AES key to use with your keyring. + // + // In practice, you should get this key from a secure key management system such as an HSM. + SecureRandom rnd = new SecureRandom(); + byte[] rawKey = new byte[32]; // 256 bits + rnd.nextBytes(rawKey); + SecretKey key = new SecretKeySpec(rawKey, "AES"); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.rawAesBuilder() + // The key namespace and key name are defined by you + // and are used by the raw AES keyring + // to determine whether it should attempt to decrypt + // an encrypted data key. + // + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + .keyNamespace("some managed raw keys") + .keyName("my AES wrapping key") + .wrappingKey(key) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/keyring/rawrsa/PublicPrivateKeySeparate.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/PublicPrivateKeySeparate.java new file mode 100644 index 000000000..0bc31b191 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/PublicPrivateKeySeparate.java @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.rawrsa; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * One of the benefits of asymmetric encryption + * is that you can encrypt with just the public key. + * This means that you can give someone the ability to encrypt + * without giving them the ability to decrypt. + *

+ * The raw RSA keyring supports encrypt-only operations + * when it only has access to a public key. + *

+ * This example shows how to construct and use the raw RSA keyring + * to encrypt with only the public key and decrypt with the private key. + *

+ * If your RSA key is in DER format, + * see the {@link RawRsaDerEncoded} example. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class PublicPrivateKeySeparate { + + /** + * Demonstrate an encrypt/decrypt cycle using separate public and private raw RSA keyrings. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final byte[] sourcePlaintext) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your keyring. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Create the keyring that determines how your data keys are protected. + // + // Create the encrypt keyring that only has access to the public key. + final Keyring publicKeyKeyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name are defined by you + // and are used by the raw RSA keyring + // to determine whether it should attempt to decrypt + // an encrypted data key. + // + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .publicKey(keyPair.getPublic()) + // The padding scheme tells the raw RSA keyring + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + .paddingScheme(RawRsaKeyringBuilder.RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Create the decrypt keyring that has access to the private key. + final Keyring privateKeyKeyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name MUST match the encrypt keyring. + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .privateKey(keyPair.getPrivate()) + // The padding scheme MUST match the encrypt keyring. + .paddingScheme(RawRsaKeyringBuilder.RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Encrypt your plaintext data using the encrypt keyring. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(publicKeyKeyring) + .encryptionContext(encryptionContext) + .plaintext(sourcePlaintext).build()); + final byte[] ciphertext = encryptResult.getResult(); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Try to decrypt your encrypted data using the *encrypt* keyring. + // This demonstrates that you cannot decrypt using the public key. + try { + awsEncryptionSdk.decrypt( + DecryptRequest.builder() + .keyring(publicKeyKeyring) + .ciphertext(ciphertext) + .build()); + throw new AssertionError("The public key can never decrypt!"); + } catch (AwsCryptoException ex) { + // The public key cannot decrypt. + // Reaching this point means everything is working as expected. + } + + // Decrypt your encrypted data using the decrypt keyring. + // + // 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() + .keyring(privateKeyKeyring) + .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/keyring/rawrsa/RawRsa.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsa.java new file mode 100644 index 000000000..3ca0a6c0e --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsa.java @@ -0,0 +1,112 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.rawrsa; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This examples shows how to configure and use a raw RSA keyring using a pre-loaded RSA key pair. + *

+ * If your RSA key is in DER format, + * see the {@link RawRsaDerEncoded} example. + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class RawRsa { + + /** + * Demonstrate an encrypt/decrypt cycle using a raw RSA keyring. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final byte[] sourcePlaintext) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your keyring. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name are defined by you + // and are used by the raw RSA keyring + // to determine whether it should attempt to decrypt + // an encrypted data key. + // + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .privateKey(keyPair.getPrivate()) + .publicKey(keyPair.getPublic()) + // The padding scheme tells the raw RSA keyring + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + .paddingScheme(RawRsaKeyringBuilder.RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/keyring/rawrsa/RawRsaDerEncoded.java b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsaDerEncoded.java new file mode 100644 index 000000000..c74c80495 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/keyring/rawrsa/RawRsaDerEncoded.java @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.keyring.rawrsa; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * When you store RSA keys, you have to serialize them somehow. + *

+ * This example shows how to configure and use a raw RSA keyring using a DER-encoded RSA private key. + *

+ * The most commonly used encodings for RSA keys tend to be PEM and DER. + * For parsing PEM-encoded keys, see https://www.bouncycastle.org/docs/pkixdocs1.4/org/bouncycastle/openssl/PEMParser.html + *

+ * https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + *

+ * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class RawRsaDerEncoded { + + /** + * Demonstrate an encrypt/decrypt cycle using a raw RSA keyring loaded from a DER-encoded key. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final byte[] sourcePlaintext) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your keyring. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Serialize the RSA keys to DER encoding. + // This or PEM encoding is likely to be what you get from your key management system in practice. + byte[] publicKeyEncoded = keyPair.getPublic().getEncoded(); + byte[] privateKeyEncoded = keyPair.getPrivate().getEncoded(); + + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + // Deserialize the RSA private key. + final PrivateKey privateKey = keyFactory.generatePrivate( + new PKCS8EncodedKeySpec(privateKeyEncoded)); + + // Deserialize the RSA public key. + final PublicKey publicKey = keyFactory.generatePublic( + new X509EncodedKeySpec(publicKeyEncoded)); + + // Create the keyring that determines how your data keys are protected. + final Keyring keyring = StandardKeyrings.rawRsaBuilder() + // The key namespace and key name are defined by you + // and are used by the raw RSA keyring + // to determine whether it should attempt to decrypt + // an encrypted data key. + // + // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + .keyNamespace("some managed raw keys") + .keyName("my RSA wrapping key") + .privateKey(privateKey) + .publicKey(publicKey) + // The padding scheme tells the raw RSA keyring + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + .paddingScheme(RawRsaKeyringBuilder.RsaPaddingScheme.OAEP_SHA256_MGF1) + .build(); + + // Encrypt your plaintext data. + final AwsCryptoResult encryptResult = awsEncryptionSdk.encrypt( + EncryptRequest.builder() + .keyring(keyring) + .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 keyring 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() + .keyring(keyring) + .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/BasicEncryptionExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/BasicEncryptionExample.java similarity index 78% rename from src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java rename to src/examples/java/com/amazonaws/crypto/examples/legacy/BasicEncryptionExample.java index 748cea536..4093004ed 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/BasicEncryptionExample.java @@ -1,17 +1,7 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 -package com.amazonaws.crypto.examples; +package com.amazonaws.crypto.examples.legacy; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -26,7 +16,10 @@ /** *

* Encrypts and then decrypts data using an AWS KMS customer master key. - * + * NOTE: Master key providers are deprecated and replaced by keyrings. + * We keep these older examples as reference material, + * but we recommend that you use the new examples in examples/keyring + * The new examples reflect our current guidance for using the library. *

* Arguments: *

    @@ -45,13 +38,13 @@ public static void main(final String[] args) { } static void encryptAndDecrypt(final String keyArn) { - // 1. Instantiate the SDK + // 1. Instantiate the AWS Encryption SDK. final AwsCrypto crypto = new AwsCrypto(); - // 2. Instantiate a KMS master key provider + // 2. Instantiate a KMS master key provider. final KmsMasterKeyProvider masterKeyProvider = KmsMasterKeyProvider.builder().withKeysForEncryption(keyArn).build(); - // 3. Create an encryption context + // 3. Create an encryption context. // // Most encrypted data should have an associated encryption context // to protect integrity. This sample uses placeholder values. @@ -60,11 +53,11 @@ static void encryptAndDecrypt(final String keyArn) { // blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management final Map encryptionContext = Collections.singletonMap("ExampleContextKey", "ExampleContextValue"); - // 4. Encrypt the data + // 4. Encrypt the data. final CryptoResult encryptResult = crypto.encryptData(masterKeyProvider, EXAMPLE_DATA, encryptionContext); final byte[] ciphertext = encryptResult.getResult(); - // 5. Decrypt the data + // 5. Decrypt the data. final CryptoResult decryptResult = crypto.decryptData(masterKeyProvider, ciphertext); // 6. Before verifying the plaintext, verify that the customer master key that @@ -82,7 +75,7 @@ static void encryptAndDecrypt(final String keyArn) { throw new IllegalStateException("Wrong Encryption Context!"); } - // 8. Verify that the decrypted plaintext matches the original plaintext + // 8. Verify that the decrypted plaintext matches the original plaintext. assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); } } diff --git a/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/EscrowedEncryptExample.java similarity index 78% rename from src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java rename to src/examples/java/com/amazonaws/crypto/examples/legacy/EscrowedEncryptExample.java index cdf72dc88..18286624f 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/EscrowedEncryptExample.java @@ -1,17 +1,7 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 -package com.amazonaws.crypto.examples; +package com.amazonaws.crypto.examples.legacy; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -32,26 +22,30 @@ /** *

    * Encrypts a file using both KMS and an asymmetric key pair. + * NOTE: Master key providers are deprecated and replaced by keyrings. + * We keep these older examples as reference material, + * but we recommend that you use the new examples in examples/keyring + * The new examples reflect our current guidance for using the library. * *

    * Arguments: *

      - *
    1. Key ARN: For help finding the Amazon Resource Name (ARN) of your KMS customer master + *
    2. Key ARN: For help finding the Amazon Resource Name (ARN) of your KMS customer master * key (CMK), see 'Viewing Keys' at http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html * - *
    3. Name of file containing plaintext data to encrypt + *
    4. Name of file containing plaintext data to encrypt *
    * - * You might use AWS Key Management Service (KMS) for most encryption and decryption operations, but - * still want the option of decrypting your data offline independently of KMS. This sample + * You might use AWS Key Management Service (KMS) for most encryption and decryption operations, but + * still want the option of decrypting your data offline independently of KMS. This sample * demonstrates one way to do this. - * + * * The sample encrypts data under both a KMS customer master key (CMK) and an "escrowed" RSA key pair - * so that either key alone can decrypt it. You might commonly use the KMS CMK for decryption. However, + * so that either key alone can decrypt it. You might commonly use the KMS CMK for decryption. However, * at any time, you can use the private RSA key to decrypt the ciphertext independent of KMS. * * This sample uses the JCEMasterKey class to generate a RSA public-private key pair - * and saves the key pair in memory. In practice, you would store the private key in a secure offline + * and saves the key pair in memory. In practice, you would store the private key in a secure offline * location, such as an offline HSM, and distribute the public key to your development team. * */ @@ -76,23 +70,23 @@ public static void main(final String[] args) throws Exception { private static void standardEncrypt(final String kmsArn, final String fileName) throws Exception { // Encrypt with the KMS CMK and the escrowed public key - // 1. Instantiate the SDK + // 1. Instantiate the AWS Encryption SDK. final AwsCrypto crypto = new AwsCrypto(); - // 2. Instantiate a KMS master key provider + // 2. Instantiate a KMS master key provider. final KmsMasterKeyProvider kms = new KmsMasterKeyProvider(kmsArn); - - // 3. Instantiate a JCE master key provider + + // 3. Instantiate a JCE master key provider. // Because the user does not have access to the private escrow key, // they pass in "null" for the private key parameter. final JceMasterKey escrowPub = JceMasterKey.getInstance(publicEscrowKey, null, "Escrow", "Escrow", "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); - // 4. Combine the providers into a single master key provider + // 4. Combine the providers into a single master key provider. final MasterKeyProvider provider = MultipleProviderFactory.buildMultiProvider(kms, escrowPub); - // 5. Encrypt the file - // To simplify the code, we omit the encryption context. Production code should always + // 5. Encrypt the file. + // To simplify the code, we omit the encryption context. Production code should always // use an encryption context. For an example, see the other SDK samples. final FileInputStream in = new FileInputStream(fileName); final FileOutputStream out = new FileOutputStream(fileName + ".encrypted"); @@ -104,26 +98,26 @@ private static void standardEncrypt(final String kmsArn, final String fileName) } private static void standardDecrypt(final String kmsArn, final String fileName) throws Exception { - // Decrypt with the KMS CMK and the escrow public key. You can use a combined provider, + // Decrypt with the KMS CMK and the escrow public key. You can use a combined provider, // as shown here, or just the KMS master key provider. - // 1. Instantiate the SDK + // 1. Instantiate the AWS Encryption SDK. final AwsCrypto crypto = new AwsCrypto(); - // 2. Instantiate a KMS master key provider + // 2. Instantiate a KMS master key provider. final KmsMasterKeyProvider kms = new KmsMasterKeyProvider(kmsArn); - - // 3. Instantiate a JCE master key provider - // Because the user does not have access to the private + + // 3. Instantiate a JCE master key provider. + // Because the user does not have access to the private // escrow key, they pass in "null" for the private key parameter. final JceMasterKey escrowPub = JceMasterKey.getInstance(publicEscrowKey, null, "Escrow", "Escrow", "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); - // 4. Combine the providers into a single master key provider + // 4. Combine the providers into a single master key provider. final MasterKeyProvider provider = MultipleProviderFactory.buildMultiProvider(kms, escrowPub); - // 5. Decrypt the file - // To simplify the code, we omit the encryption context. Production code should always + // 5. Decrypt the file. + // To simplify the code, we omit the encryption context. Production code should always // use an encryption context. For an example, see the other SDK samples. final FileInputStream in = new FileInputStream(fileName + ".encrypted"); final FileOutputStream out = new FileOutputStream(fileName + ".decrypted"); @@ -137,16 +131,16 @@ private static void escrowDecrypt(final String fileName) throws Exception { // You can decrypt the stream using only the private key. // This method does not call KMS. - // 1. Instantiate the SDK + // 1. Instantiate the AWS Encryption SDK. final AwsCrypto crypto = new AwsCrypto(); - // 2. Instantiate a JCE master key provider - // This method call uses the escrowed private key, not null + // 2. Instantiate a JCE master key provider. + // This method call uses the escrowed private key, not null. final JceMasterKey escrowPriv = JceMasterKey.getInstance(publicEscrowKey, privateEscrowKey, "Escrow", "Escrow", "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); - // 3. Decrypt the file - // To simplify the code, we omit the encryption context. Production code should always + // 3. Decrypt the file. + // To simplify the code, we omit the encryption context. Production code should always // use an encryption context. For an example, see the other SDK samples. final FileInputStream in = new FileInputStream(fileName + ".encrypted"); final FileOutputStream out = new FileOutputStream(fileName + ".deescrowed"); @@ -159,7 +153,9 @@ private static void escrowDecrypt(final String fileName) throws Exception { private static void generateEscrowKeyPair() throws GeneralSecurityException { final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); - kg.initialize(4096); // Escrow keys should be very strong + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); final KeyPair keyPair = kg.generateKeyPair(); publicEscrowKey = keyPair.getPublic(); privateEscrowKey = keyPair.getPrivate(); diff --git a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/FileStreamingExample.java similarity index 78% rename from src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java rename to src/examples/java/com/amazonaws/crypto/examples/legacy/FileStreamingExample.java index 22ade3b3b..050050972 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/FileStreamingExample.java @@ -1,17 +1,6 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.amazonaws.crypto.examples; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazonaws.crypto.examples.legacy; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -32,6 +21,10 @@ /** *

    * Encrypts and then decrypts a file under a random key. + * NOTE: Master key providers are deprecated and replaced by keyrings. + * We keep these older examples as reference material, + * but we recommend that you use the new examples in examples/keyring + * The new examples reflect our current guidance for using the library. * *

    * Arguments: @@ -49,20 +42,20 @@ public class FileStreamingExample { public static void main(String[] args) throws IOException { srcFile = args[0]; - // In this example, we generate a random key. In practice, - // you would get a key from an existing store + // In this example, we generate a random key. In practice, + // you would get a key from an existing store. SecretKey cryptoKey = retrieveEncryptionKey(); - // Create a JCE master key provider using the random key and an AES-GCM encryption algorithm + // Create a JCE master key provider using the random key and an AES-GCM encryption algorithm. JceMasterKey masterKey = JceMasterKey.getInstance(cryptoKey, "Example", "RandomKey", "AES/GCM/NoPadding"); - // Instantiate the SDK + // Instantiate the AWS Encryption SDK. AwsCrypto crypto = new AwsCrypto(); - // Create an encryption context to identify this ciphertext + // Create an encryption context to identify this ciphertext. Map context = Collections.singletonMap("Example", "FileStreaming"); - // Because the file might be to large to load into memory, we stream the data, instead of + // Because the file might be to large to load into memory, we stream the data, instead of //loading it all at once. FileInputStream in = new FileInputStream(srcFile); CryptoInputStream encryptingStream = crypto.createEncryptingStream(masterKey, in, context); @@ -80,7 +73,7 @@ public static void main(String[] args) throws IOException { throw new IllegalStateException("Bad encryption context"); } - // Return the plaintext data + // Return the plaintext data. out = new FileOutputStream(srcFile + ".decrypted"); IOUtils.copy(decryptingStream, out); decryptingStream.close(); @@ -88,7 +81,7 @@ public static void main(String[] args) throws IOException { } /** - * In practice, this key would be saved in a secure location. + * In practice, this key would be saved in a secure location. * For this demo, we generate a new random key for each operation. */ private static SecretKey retrieveEncryptionKey() { @@ -97,4 +90,4 @@ private static SecretKey retrieveEncryptionKey() { rnd.nextBytes(rawKey); return new SecretKeySpec(rawKey, "AES"); } -} \ No newline at end of file +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/legacy/LambdaDecryptAndWriteExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/LambdaDecryptAndWriteExample.java new file mode 100644 index 000000000..badadb3e0 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/LambdaDecryptAndWriteExample.java @@ -0,0 +1,128 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.legacy; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.document.DynamoDB; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; +import com.amazonaws.util.BinaryUtils; + +/** + * Decrypts all incoming Kinesis records and writes records to DynamoDB. + */ +public class LambdaDecryptAndWriteExample implements RequestHandler { + private static final long MAX_ENTRY_AGE_MILLISECONDS = 600000; + private static final int MAX_CACHE_ENTRIES = 100; + + // For best caching performance in Lambda, we want our cache to be a static final field + // configured by environment variables. + // However, to make this example easier for people to experiment with, we also provide a non-static + // version with simpler configuration. + private static final CachingCryptoMaterialsManager CACHING_CRYPTO_MATERIALS_MANAGER; + private static final String TABLE_NAME = System.getProperty("TABLE_NAME"); + + static { + final String cmkArn = System.getProperty("CMK_ARN"); + CACHING_CRYPTO_MATERIALS_MANAGER = CachingCryptoMaterialsManager.newBuilder() + .withKeyring(StandardKeyrings.awsKms(AwsKmsCmkId.fromString(cmkArn))) + .withCache(new LocalCryptoMaterialsCache(MAX_CACHE_ENTRIES)) + .withMaxAge(MAX_ENTRY_AGE_MILLISECONDS, TimeUnit.MILLISECONDS) + .build(); + } + + private final CachingCryptoMaterialsManager cachingMaterialsManager_; + private final AwsCrypto crypto_; + private final Table table_; + + /** + * No-argument constructor for use with Lambda. + * + * This is almost equivalent to calling {@link #LambdaDecryptAndWriteExample(String, String)} with + * {@code cmkArn = System.getProperty("CMK_ARN")} + * and + * {@code tableName = System.getProperty("TABLE_NAME")} + * respectively. + * The only difference is that this constructor will re-use the underlying cache across all instances + * for better cache performance. + * + * @see #LambdaDecryptAndWriteExample(String, String) + * @see #CACHING_CRYPTO_MATERIALS_MANAGER + * @see #TABLE_NAME + */ + public LambdaDecryptAndWriteExample() { + this(CACHING_CRYPTO_MATERIALS_MANAGER, TABLE_NAME); + } + + /** + * This code doesn't set the max bytes or max message security thresholds that are enforced + * only on data keys used for encryption. + * + * @param cmkArn The AWS KMS customer master key to use for decryption + * @param tableName The name of the DynamoDB table name that stores decrypted messages + */ + public LambdaDecryptAndWriteExample(final String cmkArn, final String tableName) { + this( + CachingCryptoMaterialsManager.newBuilder() + .withKeyring(StandardKeyrings.awsKms(AwsKmsCmkId.fromString(cmkArn))) + .withCache(new LocalCryptoMaterialsCache(MAX_CACHE_ENTRIES)) + .withMaxAge(MAX_ENTRY_AGE_MILLISECONDS, TimeUnit.MILLISECONDS) + .build(), + tableName); + } + + public LambdaDecryptAndWriteExample(CachingCryptoMaterialsManager cachingMatherialsManager, String tableName) { + cachingMaterialsManager_ = cachingMatherialsManager; + crypto_ = new AwsCrypto(); + table_ = new DynamoDB(AmazonDynamoDBClientBuilder.defaultClient()).getTable(tableName); + } + + /** + * Decrypts Kinesis events and writes the data to DynamoDB + * + * @param event The KinesisEvent to decrypt + * @param context The lambda context + */ + @Override + public Void handleRequest(KinesisEvent event, Context context) { + for (KinesisEventRecord record : event.getRecords()) { + ByteBuffer ciphertextBuffer = record.getKinesis().getData(); + byte[] ciphertext = BinaryUtils.copyAllBytesFrom(ciphertextBuffer); + + // Decrypt and unpack record. + AwsCryptoResult plaintextResult = crypto_.decrypt( + DecryptRequest.builder() + .cryptoMaterialsManager(cachingMaterialsManager_) + .ciphertext(ciphertext).build()); + + // Verify the encryption context value. + String streamArn = record.getEventSourceARN(); + String streamName = streamArn.substring(streamArn.indexOf("/") + 1); + if (!streamName.equals(plaintextResult.getEncryptionContext().get("stream"))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + // Write record to DynamoDB. + String jsonItem = new String(plaintextResult.getResult(), StandardCharsets.UTF_8); + table_.putItem(Item.fromJSON(jsonItem)); + } + + return null; + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/legacy/MultiRegionRecordPusherExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/MultiRegionRecordPusherExample.java new file mode 100644 index 000000000..a42e78ae1 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/MultiRegionRecordPusherExample.java @@ -0,0 +1,113 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.legacy; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +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 com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; +import com.amazonaws.regions.Region; +import com.amazonaws.services.kinesis.AmazonKinesis; +import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder; +import com.amazonaws.util.json.Jackson; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.emptyList; + +/** + * Pushes data to Kinesis Streams in multiple Regions. + */ +public class MultiRegionRecordPusherExample { + private static final long MAX_ENTRY_AGE_MILLISECONDS = 300000; + private static final long MAX_ENTRY_USES = 100; + private static final int MAX_CACHE_ENTRIES = 100; + private final String streamName_; + private final ArrayList kinesisClients_; + private final CachingCryptoMaterialsManager cachingMaterialsManager_; + private final AwsCrypto crypto_; + + /** + * Creates an instance of this object with Kinesis clients for all target Regions + * and a cached key provider containing AWS KMS master keys in all target Regions. + */ + public MultiRegionRecordPusherExample(final Region[] regions, final String kmsAliasName, final String streamName) { + streamName_ = streamName; + crypto_ = new AwsCrypto(); + kinesisClients_ = new ArrayList<>(); + + final DefaultAWSCredentialsProviderChain credentialsProvider = new DefaultAWSCredentialsProviderChain(); + + // Build AwsKmsKeyring and AmazonKinesisClient objects for each target Region. + final List keyrings = new ArrayList<>(); + + for (Region region : regions) { + kinesisClients_.add(AmazonKinesisClientBuilder.standard() + .withCredentials(credentialsProvider) + .withRegion(region.getName()) + .build()); + + keyrings.add(StandardKeyrings.awsKmsBuilder() + .awsKmsClientSupplier(StandardAwsKmsClientSuppliers + .allowRegionsBuilder(Collections.singleton(region.getName())) + .baseClientSupplier(StandardAwsKmsClientSuppliers.defaultBuilder() + .credentialsProvider(credentialsProvider).build()).build()) + .generatorKeyId(AwsKmsCmkId.fromString(kmsAliasName)).build()); + } + + // Collect keyrings into a single multi-keyring and add cache. In this example, the keyring for the + // first region is used as the generatorKeyring to generate a data key. + final List childrenKeyrings = keyrings.size() > 1 ? keyrings.subList(1, keyrings.size()) : emptyList(); + final Keyring keyring = StandardKeyrings.multi(keyrings.get(0), childrenKeyrings); + + cachingMaterialsManager_ = CachingCryptoMaterialsManager.newBuilder() + .withKeyring(keyring) + .withCache(new LocalCryptoMaterialsCache(MAX_CACHE_ENTRIES)) + .withMaxAge(MAX_ENTRY_AGE_MILLISECONDS, TimeUnit.MILLISECONDS) + .withMessageUseLimit(MAX_ENTRY_USES) + .build(); + } + + /** + * JSON serializes and encrypts the received record data and pushes it to all target streams. + */ + public void putRecord(final Map data) { + String partitionKey = UUID.randomUUID().toString(); + Map encryptionContext = Collections.singletonMap("stream", streamName_); + + // JSON serialize data. + String jsonData = Jackson.toJsonString(data); + + // Encrypt data. + AwsCryptoResult result = crypto_.encrypt( + EncryptRequest.builder() + .cryptoMaterialsManager(cachingMaterialsManager_) + .plaintext(jsonData.getBytes()) + .encryptionContext(encryptionContext) + .build()); + + byte[] encryptedData = result.getResult(); + + // Put records to Kinesis stream in all Regions. + for (AmazonKinesis regionalKinesisClient : kinesisClients_) { + regionalKinesisClient.putRecord( + streamName_, + ByteBuffer.wrap(encryptedData), + partitionKey + ); + } + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/legacy/SimpleDataKeyCachingExample.java b/src/examples/java/com/amazonaws/crypto/examples/legacy/SimpleDataKeyCachingExample.java new file mode 100644 index 000000000..90ac05335 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/legacy/SimpleDataKeyCachingExample.java @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.legacy; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.CryptoMaterialsCache; +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.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + *

    + * Encrypts a string using an AWS KMS customer master key (CMK) and data key caching + * + *

    + * Arguments: + *

      + *
    1. KMS CMK ARN: To find the Amazon Resource Name of your AWS KMS customer master key (CMK), + * see 'Viewing Keys' at http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html + *
    + */ +public class SimpleDataKeyCachingExample { + + /* + * The maximum number of data keys in the cache (required). + * When the cache is full, the oldest entry is evicted to + * make room for a newer one. + */ + private static final int CAPACITY = 10; + + /* + * The maximum number of messages encrypted under a single data key. + * This value is optional, but you should configure the lowest practical value. + */ + private static final int MAX_ENTRY_MESSAGES = 100; + + /* + * The time in seconds that an entry is cached (required). + * The cache actively removes entries that have exceeded the thresholds. + */ + private static final int MAX_ENTRY_AGE_IN_SECONDS = 60; + + /* + * Example data to encrypt + */ + private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + public static void main(final String[] args) { + encryptWithCaching(AwsKmsCmkId.fromString(args[0])); + } + + static byte[] encryptWithCaching(AwsKmsCmkId kmsCmkArn) { + + // Instantiate the AWS Encryption SDK. + final AwsCrypto crypto = new AwsCrypto(); + + // Create an encryption context. + final Map encryptionContext = Collections.singletonMap("purpose", "test"); + + // Create a keyring. + final Keyring keyring = StandardKeyrings.awsKms(kmsCmkArn); + + // Create a cache. + final CryptoMaterialsCache cache = new LocalCryptoMaterialsCache(CAPACITY); + + // Create a caching CMM. + final CryptoMaterialsManager cachingCmm = + CachingCryptoMaterialsManager.newBuilder() + .withKeyring(keyring) + .withCache(cache) + .withMaxAge(MAX_ENTRY_AGE_IN_SECONDS, TimeUnit.SECONDS) + .withMessageUseLimit(MAX_ENTRY_MESSAGES) + .build(); + + // When the call to encrypt specifies a caching CMM, + // the encryption operation uses the data key cache. + return crypto.encrypt(EncryptRequest.builder() + .cryptoMaterialsManager(cachingCmm) + .plaintext(EXAMPLE_DATA) + .encryptionContext(encryptionContext) + .build()).getResult(); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/DiscoveryDecrypt.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/DiscoveryDecrypt.java new file mode 100644 index 000000000..e16095224 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/DiscoveryDecrypt.java @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * The KMS master key provider uses any key IDs that you specify on encrypt, + * but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + * This means that you do not need to know which CMKs were used to encrypt a message. + *

    + * This example shows how to configure and use a KMS master key provider to decrypt without provider key IDs. + *

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

    + * For an example of how to use the KMS master key with a single CMK, + * see the {@link SingleCmk} example. + *

    + * For an example of how to use the KMS master key provider with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + */ +public class DiscoveryDecrypt { + + /** + * Demonstrate configuring a KMS master key provider for decryption. + * + * @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 master key that determines how your data keys are protected. + final KmsMasterKeyProvider encryptMasterKeyProvider = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsCmk.toString()).build(); + + // Create a KMS master key provider to use on decrypt. + final KmsMasterKeyProvider decryptMasterKeyProvider = KmsMasterKeyProvider.builder().build(); + + // Encrypt your plaintext data. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + encryptMasterKeyProvider, + sourcePlaintext, + encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data using the KMS master key provider. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final CryptoResult decryptResult = awsEncryptionSdk.decryptData( + decryptMasterKeyProvider, + ciphertext); + 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/masterkeyprovider/awskms/MultipleRegions.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/MultipleRegions.java new file mode 100644 index 000000000..e5cb68349 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/MultipleRegions.java @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * This example shows how to configure and use a KMS master key provider with with CMKs in multiple regions. + *

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

    + * For an example of how to use the KMS master key with a single CMK, + * see the {@link SingleCmk} example. + *

    + * For an example of how to use the KMS master key provider in discovery mode on decrypt, + * see the {@link DiscoveryDecrypt} example. + */ +public class MultipleRegions { + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS master key provider with CMKs in multiple regions. + * + * @param awsKmsGeneratorCmk The ARN of an AWS KMS CMK that protects data keys + * @param awsKmsAdditionalCmks Additional ARNs of secondary KMS CMKs + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final AwsKmsCmkId awsKmsGeneratorCmk, final List awsKmsAdditionalCmks, 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 master key provider that will encrypt your data keys under all requested CMKs. + // + // The KMS master key provider generates the data key using the first key ID in the list. + final List awsKmsCmks = new ArrayList<>(); + awsKmsCmks.add(awsKmsGeneratorCmk.toString()); + awsKmsCmks.addAll(awsKmsAdditionalCmks.stream().map(AwsKmsCmkId::toString).collect(toList())); + final KmsMasterKeyProvider masterKeyProvider = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsCmks).build(); + + // Create master key providers that each only use one of the CMKs. + // We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. + final KmsMasterKeyProvider singleCmkMasterKeyThatGenerated = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsGeneratorCmk.toString()).build(); + final KmsMasterKeyProvider singleCmkMasterKeyThatEncrypted = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsAdditionalCmks.get(0).toString()).build(); + + // Encrypt your plaintext data using the master key provider that uses all requests CMKs. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + masterKeyProvider, + sourcePlaintext, + encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // Verify that the header contains the expected number of encrypted data keys (EDKs). + // It should contain one EDK for each CMK. + assert encryptResult.getHeaders().getEncryptedKeyBlobCount() == awsKmsAdditionalCmks.size() + 1; + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data separately using the single-CMK master keys. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final CryptoResult decryptResult1 = awsEncryptionSdk.decryptData( + singleCmkMasterKeyThatGenerated, + ciphertext); + final byte[] decrypted1 = decryptResult1.getResult(); + final CryptoResult decryptResult2 = awsEncryptionSdk.decryptData( + singleCmkMasterKeyThatEncrypted, + ciphertext); + final byte[] decrypted2 = decryptResult2.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decrypted1, sourcePlaintext); + assert Arrays.equals(decrypted2, 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(decryptResult1.getEncryptionContext().get(k)); + assert v.equals(decryptResult2.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/SingleCmk.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/SingleCmk.java new file mode 100644 index 000000000..2ce88f7fd --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/awskms/SingleCmk.java @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.awskms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * This example shows how to configure and use a KMS master key with a single KMS CMK. + *

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

    + * For an example of how to use the KMS master key provider with CMKs in multiple regions, + * see the {@link MultipleRegions} example. + *

    + * For an example of how to use the KMS master key provider in discovery mode on decrypt, + * see the {@link DiscoveryDecrypt} example. + */ +public class SingleCmk { + + /** + * Demonstrate an encrypt/decrypt cycle using a KMS master key provider with a single CMK. + * + * @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 master key provider that determines how your data keys are protected. + final KmsMasterKeyProvider masterKeyProvider = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsCmk.toString()).build(); + + // Encrypt your plaintext data. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + masterKeyProvider, + sourcePlaintext, + encryptionContext); + 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 master key provider 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 CryptoResult decryptResult = awsEncryptionSdk.decryptData( + masterKeyProvider, + ciphertext); + 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/masterkeyprovider/multi/AwsKmsWithEscrow.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/multi/AwsKmsWithEscrow.java new file mode 100644 index 000000000..425f8cff6 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/multi/AwsKmsWithEscrow.java @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.multi; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * One use-case that we have seen customers need is + * the ability to enjoy the benefits of AWS KMS during normal operation + * but retain the ability to decrypt encrypted messages without access to AWS KMS. + * This example shows how you can achieve this + * by combining a KMS master key with a raw RSA master key. + *

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

    + * For more examples of how to use the KMS master key provider, see the + * 'masterkeyprovider/awskms' examples' + *

    + * For more examples of how to use the raw RSA master key, see the + * see the 'masterkeyprovider/rawrsa' examples. + *

    + * In this example we generate a RSA keypair + * but in practice you would want to keep your private key in an HSM + * or other key management system. + *

    + * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class AwsKmsWithEscrow { + + /** + * Demonstrate configuring a master key provider to use an AWS KMS CMK and a RSA wrapping key. + * + * @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) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your master key. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Create the encrypt master key that only has access to the public key. + final JceMasterKey escrowEncryptMasterKey = JceMasterKey.getInstance( + keyPair.getPublic(), + null, // Private key is null + // The provider ID and key ID are defined by you + // and are used by the raw RSA master key + // to determine whether it should attempt to decrypt + // an encrypted data key. + "some managed raw keys", // provider corresponds to key namespace for keyrings + "my RSA wrapping key", // key ID corresponds to key name for keyrings + // The padding scheme tells the raw RSA master key + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + // Create the decrypt master key that has access to the private key. + final JceMasterKey escrowDecryptMasterKey = JceMasterKey.getInstance( + null, // Public key is null + keyPair.getPrivate(), + // The key namespace and key name MUST match the encrypt master key. + "some managed raw keys", // provider corresponds to key namespace for keyrings + "my RSA wrapping key", // key ID corresponds to key name for keyrings + // The wrapping algorithm MUST match the encrypt master key. + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + + // Create the KMS master key that you will use for decryption during normal operations. + final KmsMasterKeyProvider kmsMasterKeyProvider = KmsMasterKeyProvider.builder() + .withKeysForEncryption(awsKmsCmk.toString()).build(); + + // Combine the KMS and escrow providers into a single master key provider. + final MasterKeyProvider masterKeyProvider = MultipleProviderFactory.buildMultiProvider( + kmsMasterKeyProvider, escrowEncryptMasterKey); + + // Encrypt your plaintext data using the combined master keys. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + masterKeyProvider, + sourcePlaintext, + encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // Verify that the header contains the expected number of encrypted data keys (EDKs). + // It should contain one EDK for KMS and one for the escrow key. + assert encryptResult.getHeaders().getEncryptedKeyBlobCount() == 2; + + // Demonstrate that the ciphertext and plaintext are different. + assert !Arrays.equals(ciphertext, sourcePlaintext); + + // Decrypt your encrypted data separately using the KMS master key provider + // and the escrow decrypt master key. + // + // You do not need to specify the encryption context on decrypt because + // the header of the encrypted message includes the encryption context. + final CryptoResult decryptedKmsResult = awsEncryptionSdk.decryptData( + kmsMasterKeyProvider, + ciphertext); + final byte[] decryptedKms = decryptedKmsResult.getResult(); + final CryptoResult decryptedEscrowResult = awsEncryptionSdk.decryptData( + escrowDecryptMasterKey, + ciphertext); + final byte[] decryptedEscrow = decryptedKmsResult.getResult(); + + // Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert Arrays.equals(decryptedKms, sourcePlaintext); + assert Arrays.equals(decryptedEscrow, 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(decryptedKmsResult.getEncryptionContext().get(k)); + assert v.equals(decryptedEscrowResult.getEncryptionContext().get(k)); + }); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/rawaes/RawAes.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/rawaes/RawAes.java new file mode 100644 index 000000000..89a8d90cc --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/rawaes/RawAes.java @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.rawaes; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * This examples shows how to configure and use a raw AES master key. + *

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

    + * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class RawAes { + + /** + * Demonstrate an encrypt/decrypt cycle using a raw AES master key. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(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"); + + // Generate an AES key to use with your master key. + // + // In practice, you should get this key from a secure key management system such as an HSM. + SecureRandom rnd = new SecureRandom(); + byte[] rawKey = new byte[32]; // 256 bits + rnd.nextBytes(rawKey); + SecretKey key = new SecretKeySpec(rawKey, "AES"); + + // Create the master key that determines how your data keys are protected. + final JceMasterKey masterKey = JceMasterKey.getInstance( + // The key namespace and key name are defined by you + // and are used by the raw AES master key + // to determine whether it should attempt to decrypt + // an encrypted data key. + key, + "some managed raw keys", // provider corresponds to key namespace for keyrings + "my AES wrapping key", // key ID corresponds to key name for keyrings + "AES/GCM/NOPADDING"); // the AES JceMasterKey only supports AES/GCM/NOPADDING + + // Encrypt your plaintext data. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + masterKey, + sourcePlaintext, + encryptionContext); + 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 master key 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 CryptoResult decryptResult = awsEncryptionSdk.decryptData( + masterKey, + ciphertext); + 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/masterkeyprovider/rawrsa/RawRsa.java b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/rawrsa/RawRsa.java new file mode 100644 index 000000000..ff1d972f0 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/masterkeyprovider/rawrsa/RawRsa.java @@ -0,0 +1,104 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples.masterkeyprovider.rawrsa; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * This example is intended to serve as reference material for users migrating away from master key providers. + * We recommend using keyrings rather than master key providers. + * For examples using keyrings, see the 'examples/keyring' directory. + *

    + * This examples shows how to configure and use a raw RSA master key using a pre-loaded RSA key pair. + *

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

    + * In this example, we use the one-step encrypt and decrypt APIs. + */ +public class RawRsa { + + /** + * Demonstrate an encrypt/decrypt cycle using a raw RSA master key. + * + * @param sourcePlaintext Plaintext to encrypt + */ + public static void run(final byte[] sourcePlaintext) throws GeneralSecurityException { + // 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"); + + // Generate an RSA key pair to use with your master key. + // In practice, you should get this key from a secure key management system such as an HSM. + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + // The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + // https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + kg.initialize(4096); + final KeyPair keyPair = kg.generateKeyPair(); + + // Create the master key that determines how your data keys are protected. + final JceMasterKey masterKey = JceMasterKey.getInstance( + keyPair.getPublic(), + keyPair.getPrivate(), + // The provider ID and key ID are defined by you + // and are used by the raw RSA master key + // to determine whether it should attempt to decrypt + // an encrypted data key. + "some managed raw keys", // provider corresponds to key namespace for keyrings + "my RSA wrapping key", // key ID corresponds to key name for keyrings + // The padding scheme tells the raw RSA master key + // how to use your wrapping key to encrypt data keys. + // + // We recommend using OAEP_SHA256_MGF1. + // You should not use PKCS1 unless you require it for backwards compatibility. + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + // Encrypt your plaintext data. + final CryptoResult encryptResult = awsEncryptionSdk.encryptData( + masterKey, + sourcePlaintext, + encryptionContext); + 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 master key 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 CryptoResult decryptResult = awsEncryptionSdk.decryptData( + masterKey, + ciphertext); + 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/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java index fecd7333c..61d185fe8 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; +import java.util.function.Consumer; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.BadCiphertextException; @@ -27,31 +28,46 @@ import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; import com.amazonaws.encryptionsdk.internal.ProcessingSummary; import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import com.amazonaws.encryptionsdk.model.EncryptionMaterials; import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; +import static java.util.Objects.requireNonNull; + /** * Provides the primary entry-point to the AWS Encryption SDK. All encryption and decryption * operations should start here. Most people will want to use either - * {@link #encryptData(MasterKeyProvider, byte[], Map)} and - * {@link #decryptData(MasterKeyProvider, byte[])} to encrypt/decrypt things. + * {@link #encrypt(EncryptRequest)} and + * {@link #decrypt(DecryptRequest)} to encrypt/decrypt things. * *

    * The core concepts (and classes) in this SDK are: *

      *
    • {@link AwsCrypto} - *
    • {@link DataKey} + *
    • {@link Keyring} + *
    • {@link EncryptedDataKey} *
    • {@link MasterKey} *
    • {@link MasterKeyProvider} *
    * *

    * {@link AwsCrypto} provides the primary way to encrypt/decrypt data. It can operate on - * byte-arrays, streams, or {@link java.lang.String Strings}. This data is encrypted using the - * specifed {@link CryptoAlgorithm} and a {@link DataKey} which is unique to each encrypted message. - * This {@code DataKey} is then encrypted using one (or more) {@link MasterKey MasterKeys}. The - * process is reversed on decryption with the code selecting a copy of the {@code DataKey} protected - * by a usable {@code MasterKey}, decrypting the {@code DataKey}, and then decrypted the message. + * byte-arrays and streams. This data is encrypted using the specified {@link CryptoAlgorithm} and a + * {@code plaintextDataKey} which is unique to each encrypted message. This {@code plaintextDataKey} is then encrypted + * using one (or more) {@link MasterKey MasterKeys} or a {@link Keyring}. The process is reversed on decryption with the + * code selecting an {@code EncryptedDataKey} embedded in the encrypted message, decrypting the {@code EncryptedDataKey} + * using a {@link MasterKey MasterKey} or {@link Keyring}, and then decrypting the message. + * + *

    + * Note: + * {@link MasterKey}s and {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. Keyrings + * provide a simplified architecture over {@link MasterKey}s and {@link MasterKeyProvider}s; The keyring combines the + * similar responsibilities of {@link MasterKey}s and {@link MasterKeyProvider}s into one concept, as well as removes + * all key management logic from {@link CryptoMaterialsManager}s. + * + * Due to this simplified architecture, {@link MasterKey}s and {@link MasterKeyProvider}s are deprecated and will be + * removed in the future, and new implementations should use {@link Keyring}s. The below discussion on + * {@link MasterKeyProvider}s is provided for informational purposes as you transition to {@link Keyring}s. * *

    * The main way to get a {@code MasterKey} is through the use of a {@link MasterKeyProvider}. This @@ -150,7 +166,11 @@ public int getEncryptionFrameSize() { * * This method is equivalent to calling {@link #estimateCiphertextSize(CryptoMaterialsManager, int, Map)} with a * {@link DefaultCryptoMaterialsManager} based on the given provider. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #estimateCiphertextSize(EstimateCiphertextSizeRequest)} */ + @Deprecated public > long estimateCiphertextSize( final MasterKeyProvider provider, final int plaintextSize, @@ -162,34 +182,31 @@ public > long estimateCiphertextSize( /** * Returns the best estimate for the output length of encrypting a plaintext with the provided * {@code plaintextSize} and {@code encryptionContext}. The actual ciphertext may be shorter. + * + * @deprecated Replaced by {@link #estimateCiphertextSize(EstimateCiphertextSizeRequest)} */ + @Deprecated public long estimateCiphertextSize( CryptoMaterialsManager materialsManager, final int plaintextSize, final Map encryptionContext ) { - EncryptionMaterialsRequest request = EncryptionMaterialsRequest.newBuilder() - .setContext(encryptionContext) - .setRequestedAlgorithm(getEncryptionAlgorithm()) - // We're not actually encrypting any data, so don't consume any bytes from the cache's limits. We do need to - // pass /something/ though, or the cache will be bypassed (as it'll assume this is a streaming encrypt of - // unknown size). - .setPlaintextSize(0) - .build(); - - final MessageCryptoHandler cryptoHandler = new EncryptionHandler( - getEncryptionFrameSize(), - checkAlgorithm(materialsManager.getMaterialsForEncrypt(request)) - ); - - return cryptoHandler.estimateOutputSize(plaintextSize); + return estimateCiphertextSize(EstimateCiphertextSizeRequest.builder() + .cryptoMaterialsManager(materialsManager) + .encryptionContext(encryptionContext) + .plaintextSize(plaintextSize) + .build()); } /** * Returns the equivalent to calling * {@link #estimateCiphertextSize(MasterKeyProvider, int, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #estimateCiphertextSize(EstimateCiphertextSizeRequest)} */ + @Deprecated public > long estimateCiphertextSize( final MasterKeyProvider provider, final int plaintextSize @@ -201,7 +218,10 @@ public > long estimateCiphertextSize( * Returns the equivalent to calling * {@link #estimateCiphertextSize(CryptoMaterialsManager, int, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated Replaced by {@link #estimateCiphertextSize(EstimateCiphertextSizeRequest)} */ + @Deprecated public long estimateCiphertextSize( final CryptoMaterialsManager materialsManager, final int plaintextSize @@ -210,13 +230,59 @@ public long estimateCiphertextSize( } /** - * Returns an encrypted form of {@code plaintext} that has been protected with {@link DataKey - * DataKeys} that are in turn protected by {@link MasterKey MasterKeys} provided by + * Returns the best estimate for the output length of encrypting a plaintext with the plaintext size specified in + * the provided {@link EstimateCiphertextSizeRequest}. The actual ciphertext may be shorter. + * + * @param request The {@link EstimateCiphertextSizeRequest} + * @return The estimated output length in bytes + */ + public long estimateCiphertextSize(final EstimateCiphertextSizeRequest request) { + requireNonNull(request, "request is required"); + + final EncryptionMaterialsRequest encryptionMaterialsRequest = EncryptionMaterialsRequest.newBuilder() + .setContext(request.encryptionContext()) + .setRequestedAlgorithm(getEncryptionAlgorithm()) + // We're not actually encrypting any data, so don't consume any bytes from the cache's limits. We do need to + // pass /something/ though, or the cache will be bypassed (as it'll assume this is a streaming encrypt of + // unknown size). + .setPlaintextSize(0) + .build(); + + final MessageCryptoHandler cryptoHandler = new EncryptionHandler( + getEncryptionFrameSize(), + checkAlgorithm(request.cryptoMaterialsManager().getMaterialsForEncrypt(encryptionMaterialsRequest)) + ); + + return cryptoHandler.estimateOutputSize(request.plaintextSize()); + } + + /** + * Returns the best estimate for the output length of encrypting a plaintext with the plaintext size specified in + * the provided {@link EstimateCiphertextSizeRequest}. The actual ciphertext may be shorter. + *

    + * This is a convenience which creates an instance of the {@link EstimateCiphertextSizeRequest.Builder} avoiding the need to + * create one manually via {@link EstimateCiphertextSizeRequest#builder()} + *

    + * + * @param request A Consumer that will call methods on EstimateCiphertextSizeRequest.Builder to create a request. + * @return The estimated output length in bytes + */ + public long estimateCiphertextSize(final Consumer request) { + return estimateCiphertextSize(EstimateCiphertextSizeRequest.builder().applyMutation(request).build()); + } + + /** + * Returns an encrypted form of {@code plaintext} that has been protected with {@link DataKey}s + * that are in turn protected by {@link MasterKey MasterKeys} provided by * {@code provider}. * * This method is equivalent to calling {@link #encryptData(CryptoMaterialsManager, byte[], Map)} using a * {@link DefaultCryptoMaterialsManager} based on the given provider. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #encrypt(EncryptRequest)} */ + @Deprecated public > CryptoResult encryptData( final MasterKeyProvider provider, final byte[] plaintext, @@ -230,38 +296,29 @@ public > CryptoResult encryptData( /** * Returns an encrypted form of {@code plaintext} that has been protected with {@link DataKey * DataKeys} that are in turn protected by the given CryptoMaterialsProvider. + * + * @deprecated Replaced by {@link #encrypt(EncryptRequest)} */ + @Deprecated public CryptoResult encryptData( CryptoMaterialsManager materialsManager, final byte[] plaintext, final Map encryptionContext ) { - EncryptionMaterialsRequest request = EncryptionMaterialsRequest.newBuilder() - .setContext(encryptionContext) - .setRequestedAlgorithm(getEncryptionAlgorithm()) - .setPlaintext(plaintext) - .build(); - - final MessageCryptoHandler cryptoHandler = new EncryptionHandler( - getEncryptionFrameSize(), - checkAlgorithm(materialsManager.getMaterialsForEncrypt(request)) - ); - - final int outSizeEstimate = cryptoHandler.estimateOutputSize(plaintext.length); - final byte[] out = new byte[outSizeEstimate]; - int outLen = cryptoHandler.processBytes(plaintext, 0, plaintext.length, out, 0).getBytesWritten(); - outLen += cryptoHandler.doFinal(out, outLen); - - final byte[] outBytes = Utils.truncate(out, outLen); - - //noinspection unchecked - return new CryptoResult(outBytes, cryptoHandler.getMasterKeys(), cryptoHandler.getHeaders()); + return encrypt(EncryptRequest.builder() + .cryptoMaterialsManager(materialsManager) + .plaintext(plaintext) + .encryptionContext(encryptionContext).build()).toCryptoResult(); } /** * Returns the equivalent to calling {@link #encryptData(MasterKeyProvider, byte[], Map)} with * an empty {@code encryptionContext}. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #encrypt(EncryptRequest)} */ + @Deprecated public > CryptoResult encryptData(final MasterKeyProvider provider, final byte[] plaintext) { return encryptData(provider, plaintext, EMPTY_MAP); @@ -270,7 +327,10 @@ public > CryptoResult encryptData(final Master /** * Returns the equivalent to calling {@link #encryptData(CryptoMaterialsManager, byte[], Map)} with * an empty {@code encryptionContext}. + * + * @deprecated Replaced by {@link #encrypt(EncryptRequest)} */ + @Deprecated public CryptoResult encryptData( final CryptoMaterialsManager materialsManager, final byte[] plaintext @@ -278,11 +338,59 @@ public > CryptoResult encryptData(final Master return encryptData(materialsManager, plaintext, EMPTY_MAP); } + /** + * Returns an encrypted form of {@code plaintext} that has been protected with either the {@link CryptoMaterialsManager} + * or the {@link Keyring} specified in the {@link EncryptRequest}. + * + * @param request The {@link EncryptRequest} + * @return An {@link AwsCryptoResult} containing the encrypted data + */ + public AwsCryptoResult encrypt(final EncryptRequest request) { + requireNonNull(request, "request is required"); + + final EncryptionMaterialsRequest encryptionMaterialsRequest = EncryptionMaterialsRequest.newBuilder() + .setContext(request.encryptionContext()) + .setRequestedAlgorithm(getEncryptionAlgorithm()) + .setPlaintext(request.plaintext()) + .build(); + + final EncryptionMaterials encryptionMaterials = + checkAlgorithm(request.cryptoMaterialsManager().getMaterialsForEncrypt(encryptionMaterialsRequest)); + + final MessageCryptoHandler cryptoHandler = new EncryptionHandler( + getEncryptionFrameSize(), encryptionMaterials); + + final int outSizeEstimate = cryptoHandler.estimateOutputSize(request.plaintext().length); + final byte[] out = new byte[outSizeEstimate]; + int outLen = cryptoHandler.processBytes(request.plaintext(), 0, request.plaintext().length, out, 0).getBytesWritten(); + outLen += cryptoHandler.doFinal(out, outLen); + + final byte[] outBytes = Utils.truncate(out, outLen); + + return new AwsCryptoResult<>(outBytes, encryptionMaterials.getKeyringTrace(), + cryptoHandler.getMasterKeys(), cryptoHandler.getHeaders()); + } + + /** + * Returns an encrypted form of {@code plaintext} that has been protected with either the {@link CryptoMaterialsManager} + * or the {@link Keyring} specified in the {@link EncryptRequest}. + *

    + * This is a convenience which creates an instance of the {@link EncryptRequest.Builder} avoiding the need to + * create one manually via {@link EncryptRequest#builder()} + *

    + * + * @param request A Consumer that will call methods on EncryptRequest.Builder to create a request. + * @return An {@link AwsCryptoResult} containing the encrypted data + */ + public AwsCryptoResult encrypt(final Consumer request) { + return encrypt(EncryptRequest.builder().applyMutation(request).build()); + } + /** * Calls {@link #encryptData(MasterKeyProvider, byte[], Map)} on the UTF-8 encoded bytes of * {@code plaintext} and base64 encodes the result. - * @deprecated Use the {@link #encryptData(MasterKeyProvider, byte[], Map)} and - * {@link #decryptData(MasterKeyProvider, byte[])} APIs instead. {@code encryptString} and {@code decryptString} + * @deprecated Use the {@link #encrypt(EncryptRequest)} and + * {@link #decrypt(DecryptRequest)} APIs instead. {@code encryptString} and {@code decryptString} * work as expected if you use them together. However, to work with other language implementations of the AWS * Encryption SDK, you need to base64-decode the output of {@code encryptString} and base64-encode the input to * {@code decryptString}. These deprecated APIs will be removed in the future. @@ -358,7 +466,11 @@ public > CryptoResult encryptString(final Mast * Decrypts the provided {@code ciphertext} by requesting that the {@code provider} unwrap any * usable {@link DataKey} in the ciphertext and then decrypts the ciphertext using that * {@code DataKey}. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #decrypt(DecryptRequest)} */ + @Deprecated public > CryptoResult decryptData(final MasterKeyProvider provider, final byte[] ciphertext) { return decryptData(Utils.assertNonNull(provider, "provider"), new @@ -372,7 +484,10 @@ public > CryptoResult decryptData(final Master * @param materialsManager the {@link CryptoMaterialsManager} to use for decryption operations. * @param ciphertext the ciphertext to attempt to decrypt. * @return the {@link CryptoResult} with the decrypted data. + * + * @deprecated Replaced by {@link #decrypt(DecryptRequest)} */ + @Deprecated public CryptoResult decryptData( final CryptoMaterialsManager materialsManager, final byte[] ciphertext @@ -383,8 +498,12 @@ public > CryptoResult decryptData(final Master /** * @see #decryptData(MasterKeyProvider, byte[]) + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #decrypt(DecryptRequest)} */ @SuppressWarnings("unchecked") + @Deprecated public > CryptoResult decryptData( final MasterKeyProvider provider, final ParsedCiphertext ciphertext) { Utils.assertNonNull(provider, "provider"); @@ -393,22 +512,38 @@ public > CryptoResult decryptData( /** * @see #decryptData(CryptoMaterialsManager, byte[]) + * + * @deprecated Replaced by {@link #decrypt(DecryptRequest)} */ + @Deprecated public CryptoResult decryptData( final CryptoMaterialsManager materialsManager, final ParsedCiphertext ciphertext ) { - Utils.assertNonNull(materialsManager, "materialsManager"); + return decrypt(DecryptRequest.builder() + .cryptoMaterialsManager(materialsManager) + .parsedCiphertext(ciphertext).build()).toCryptoResult(); + } - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, ciphertext); + /** + * Decrypts the provided {@link ParsedCiphertext} using the {@link CryptoMaterialsManager} or the {@link Keyring} + * specified in the {@link DecryptRequest}. + * + * @param request The {@link DecryptRequest} + * @return An {@link AwsCryptoResult} containing the decrypted data + */ + public AwsCryptoResult decrypt(final DecryptRequest request) { + requireNonNull(request, "request is required"); - final byte[] ciphertextBytes = ciphertext.getCiphertext(); - final int contentLen = ciphertextBytes.length - ciphertext.getOffset(); + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create( + request.cryptoMaterialsManager(), request.parsedCiphertext()); + + final byte[] ciphertextBytes = request.parsedCiphertext().getCiphertext(); + final int contentLen = ciphertextBytes.length - request.parsedCiphertext().getOffset(); final int outSizeEstimate = cryptoHandler.estimateOutputSize(contentLen); final byte[] out = new byte[outSizeEstimate]; - final ProcessingSummary processed = cryptoHandler.processBytes(ciphertextBytes, ciphertext.getOffset(), - contentLen, out, - 0); + final ProcessingSummary processed = cryptoHandler.processBytes(ciphertextBytes, request.parsedCiphertext().getOffset(), + contentLen, out, 0); if (processed.getBytesProcessed() != contentLen) { throw new BadCiphertextException("Unable to process entire ciphertext. May have trailing data."); } @@ -417,8 +552,23 @@ public > CryptoResult decryptData( final byte[] outBytes = Utils.truncate(out, outLen); - //noinspection unchecked - return new CryptoResult(outBytes, cryptoHandler.getMasterKeys(), cryptoHandler.getHeaders()); + return new AwsCryptoResult<>(outBytes, cryptoHandler.getKeyringTrace(), + cryptoHandler.getMasterKeys(), cryptoHandler.getHeaders()); + } + + /** + * Decrypts the provided {@link ParsedCiphertext} using the {@link CryptoMaterialsManager} or the {@link Keyring} + * specified in the {@link DecryptRequest}. + *

    + * This is a convenience which creates an instance of the {@link DecryptRequest.Builder} avoiding the need to + * create one manually via {@link DecryptRequest#builder()} + *

    + * + * @param request A Consumer that will call methods on DecryptRequest.Builder to create a request. + * @return An {@link AwsCryptoResult} containing the decrypted data + */ + public AwsCryptoResult decrypt(final Consumer request) { + return decrypt(DecryptRequest.builder().applyMutation(request).build()); } /** @@ -475,7 +625,11 @@ public > CryptoResult decryptString( * * @see #encryptData(MasterKeyProvider, byte[], Map) * @see javax.crypto.CipherOutputStream + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createEncryptingOutputStream(CreateEncryptingOutputStreamRequest)} */ + @Deprecated public > CryptoOutputStream createEncryptingStream( final MasterKeyProvider provider, final OutputStream os, @@ -492,20 +646,31 @@ public > CryptoOutputStream createEncryptingStream( * * @see #encryptData(MasterKeyProvider, byte[], Map) * @see javax.crypto.CipherOutputStream + * + * @deprecated Replaced by {@link #createEncryptingOutputStream(CreateEncryptingOutputStreamRequest)} */ + @Deprecated public CryptoOutputStream createEncryptingStream( final CryptoMaterialsManager materialsManager, final OutputStream os, final Map encryptionContext ) { - return new CryptoOutputStream<>(os, getEncryptingStreamHandler(materialsManager, encryptionContext)); + return createEncryptingOutputStream(CreateEncryptingOutputStreamRequest.builder() + .cryptoMaterialsManager(materialsManager) + .outputStream(os) + .encryptionContext(encryptionContext) + .build()).toCryptoOutputStream(); } /** * Returns the equivalent to calling * {@link #createEncryptingStream(MasterKeyProvider, OutputStream, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createEncryptingOutputStream(CreateEncryptingOutputStreamRequest)} */ + @Deprecated public > CryptoOutputStream createEncryptingStream( final MasterKeyProvider provider, final OutputStream os) { @@ -516,7 +681,10 @@ public > CryptoOutputStream createEncryptingStream( * Returns the equivalent to calling * {@link #createEncryptingStream(CryptoMaterialsManager, OutputStream, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated Replaced by {@link #createEncryptingOutputStream(CreateEncryptingOutputStreamRequest)} */ + @Deprecated public CryptoOutputStream createEncryptingStream( final CryptoMaterialsManager materialsManager, final OutputStream os @@ -524,13 +692,44 @@ public CryptoOutputStream createEncryptingStream( return createEncryptingStream(materialsManager, os, EMPTY_MAP); } + /** + * Returns a {@link AwsCryptoOutputStream} which encrypts the data prior to passing it onto the + * underlying {@link OutputStream}. + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherOutputStream + */ + public AwsCryptoOutputStream createEncryptingOutputStream(final CreateEncryptingOutputStreamRequest request) { + return new AwsCryptoOutputStream(request.outputStream(), getEncryptingStreamHandler( + request.cryptoMaterialsManager(), request.encryptionContext())); + } + + /** + * Returns a {@link AwsCryptoOutputStream} which encrypts the data prior to passing it onto the + * underlying {@link OutputStream}. + *

    + * This is a convenience which creates an instance of the {@link CreateEncryptingOutputStreamRequest.Builder} avoiding the need to + * create one manually via {@link CreateEncryptingOutputStreamRequest#builder()} + *

    + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherOutputStream + */ + public AwsCryptoOutputStream createEncryptingOutputStream(final Consumer request) { + return createEncryptingOutputStream(CreateEncryptingOutputStreamRequest.builder().applyMutation(request).build()); + } + /** * Returns a {@link CryptoInputStream} which encrypts the data after reading it from the * underlying {@link InputStream}. * * @see #encryptData(MasterKeyProvider, byte[], Map) * @see javax.crypto.CipherInputStream + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createEncryptingInputStream(CreateEncryptingInputStreamRequest)} */ + @Deprecated public > CryptoInputStream createEncryptingStream( final MasterKeyProvider provider, final InputStream is, @@ -547,22 +746,30 @@ public > CryptoInputStream createEncryptingStream( * * @see #encryptData(MasterKeyProvider, byte[], Map) * @see javax.crypto.CipherInputStream + * + * @deprecated Replaced by {@link #createEncryptingInputStream(CreateEncryptingInputStreamRequest)} */ + @Deprecated public CryptoInputStream createEncryptingStream( CryptoMaterialsManager materialsManager, final InputStream is, final Map encryptionContext ) { - final MessageCryptoHandler cryptoHandler = getEncryptingStreamHandler(materialsManager, encryptionContext); - - return new CryptoInputStream<>(is, cryptoHandler); + return createEncryptingInputStream(CreateEncryptingInputStreamRequest.builder() + .cryptoMaterialsManager(materialsManager) + .encryptionContext(encryptionContext) + .inputStream(is).build()).toCryptoInputStream(); } /** * Returns the equivalent to calling * {@link #createEncryptingStream(MasterKeyProvider, InputStream, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createEncryptingInputStream(CreateEncryptingInputStreamRequest)} */ + @Deprecated public > CryptoInputStream createEncryptingStream( final MasterKeyProvider provider, final InputStream is @@ -574,12 +781,47 @@ public > CryptoInputStream createEncryptingStream( * Returns the equivalent to calling * {@link #createEncryptingStream(CryptoMaterialsManager, InputStream, Map)} with an empty * {@code encryptionContext}. + * + * @deprecated Replaced by {@link #createEncryptingInputStream(CreateEncryptingInputStreamRequest)} */ + @Deprecated public CryptoInputStream createEncryptingStream( final CryptoMaterialsManager materialsManager, final InputStream is ) { - return createEncryptingStream(materialsManager, is, EMPTY_MAP); + return createEncryptingInputStream(CreateEncryptingInputStreamRequest.builder() + .cryptoMaterialsManager(materialsManager) + .inputStream(is).build()).toCryptoInputStream(); + } + + /** + * Returns a {@link AwsCryptoInputStream} which encrypts the data after reading it from the + * underlying {@link InputStream}. + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherInputStream + */ + public AwsCryptoInputStream createEncryptingInputStream(final CreateEncryptingInputStreamRequest request) { + requireNonNull(request, "request is required"); + final MessageCryptoHandler cryptoHandler = getEncryptingStreamHandler( + request.cryptoMaterialsManager(), request.encryptionContext()); + + return new AwsCryptoInputStream(request.inputStream(), cryptoHandler); + } + + /** + * Returns a {@link AwsCryptoInputStream} which encrypts the data after reading it from the + * underlying {@link InputStream}. + *

    + * This is a convenience which creates an instance of the {@link CreateEncryptingInputStreamRequest.Builder} avoiding the need to + * create one manually via {@link CreateEncryptingInputStreamRequest#builder()} + *

    + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherInputStream + */ + public AwsCryptoInputStream createEncryptingInputStream(final Consumer request) { + return createEncryptingInputStream(CreateEncryptingInputStreamRequest.builder().applyMutation(request).build()); } /** @@ -588,7 +830,11 @@ public CryptoInputStream createEncryptingStream( * * @see #decryptData(MasterKeyProvider, byte[]) * @see javax.crypto.CipherOutputStream + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createDecryptingOutputStream(CreateDecryptingOutputStreamRequest)} */ + @Deprecated public > CryptoOutputStream createDecryptingStream( final MasterKeyProvider provider, final OutputStream os) { final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider); @@ -601,7 +847,11 @@ public > CryptoOutputStream createDecryptingStream( * * @see #decryptData(MasterKeyProvider, byte[]) * @see javax.crypto.CipherInputStream + * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * Replaced by {@link #createDecryptingInputStream(CreateDecryptingInputStreamRequest)} */ + @Deprecated public > CryptoInputStream createDecryptingStream( final MasterKeyProvider provider, final InputStream is) { final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider); @@ -614,12 +864,44 @@ public > CryptoInputStream createDecryptingStream( * * @see #decryptData(CryptoMaterialsManager, byte[]) * @see javax.crypto.CipherOutputStream + * + * @deprecated Replaced by {@link #createDecryptingOutputStream(CreateDecryptingOutputStreamRequest)} */ + @Deprecated public CryptoOutputStream createDecryptingStream( final CryptoMaterialsManager materialsManager, final OutputStream os ) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager); - return new CryptoOutputStream(os, cryptoHandler); + return createDecryptingOutputStream(CreateDecryptingOutputStreamRequest.builder() + .cryptoMaterialsManager(materialsManager) + .outputStream(os).build()).toCryptoOutputStream(); + } + + /** + * Returns a {@link AwsCryptoOutputStream} which decrypts the data prior to passing it onto the + * underlying {@link OutputStream}. + * + * @see #decrypt(DecryptRequest) + * @see javax.crypto.CipherOutputStream + */ + public AwsCryptoOutputStream createDecryptingOutputStream(final CreateDecryptingOutputStreamRequest request) { + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(request.cryptoMaterialsManager()); + return new AwsCryptoOutputStream(request.outputStream(), cryptoHandler); + } + + + /** + * Returns a {@link AwsCryptoOutputStream} which decrypts the data prior to passing it onto the + * underlying {@link OutputStream}. + *

    + * This is a convenience which creates an instance of the {@link CreateDecryptingOutputStreamRequest.Builder} avoiding the need to + * create one manually via {@link CreateDecryptingOutputStreamRequest#builder()} + *

    + * + * @see #decrypt(DecryptRequest) + * @see javax.crypto.CipherOutputStream + */ + public AwsCryptoOutputStream createDecryptingOutputStream(final Consumer request) { + return createDecryptingOutputStream(CreateDecryptingOutputStreamRequest.builder().applyMutation(request).build()); } /** @@ -628,12 +910,45 @@ public CryptoOutputStream createDecryptingStream( * * @see #encryptData(CryptoMaterialsManager, byte[], Map) * @see javax.crypto.CipherInputStream + * + * @deprecated Replaced by {@link #createDecryptingInputStream(CreateDecryptingInputStreamRequest)} */ + @Deprecated public CryptoInputStream createDecryptingStream( final CryptoMaterialsManager materialsManager, final InputStream is ) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager); - return new CryptoInputStream(is, cryptoHandler); + return createDecryptingInputStream(CreateDecryptingInputStreamRequest.builder() + .cryptoMaterialsManager(materialsManager) + .inputStream(is).build()).toCryptoInputStream(); + } + + /** + * Returns a {@link AwsCryptoInputStream} which decrypts the data after reading it from the + * underlying {@link InputStream}. + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherInputStream + */ + public AwsCryptoInputStream createDecryptingInputStream(final CreateDecryptingInputStreamRequest request) { + requireNonNull(request, "request is required"); + + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(request.cryptoMaterialsManager()); + return new AwsCryptoInputStream(request.inputStream(), cryptoHandler); + } + + /** + * Returns a {@link AwsCryptoInputStream} which decrypts the data after reading it from the + * underlying {@link InputStream}. + *

    + * This is a convenience which creates an instance of the {@link CreateDecryptingInputStreamRequest.Builder} avoiding the need to + * create one manually via {@link CreateDecryptingInputStreamRequest#builder()} + *

    + * + * @see #encrypt(EncryptRequest) + * @see javax.crypto.CipherInputStream + */ + public AwsCryptoInputStream createDecryptingInputStream(final Consumer request) { + return createDecryptingInputStream(CreateDecryptingInputStreamRequest.builder().applyMutation(request).build()); } private MessageCryptoHandler getEncryptingStreamHandler( diff --git a/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoInputStream.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoInputStream.java new file mode 100644 index 000000000..81f56bde4 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoInputStream.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An AwsCryptoInputStream is a subclass of java.io.InputStream. It performs cryptographic + * transformation of the bytes passing through it. + * + *

    + * The AwsCryptoInputStream wraps a provided InputStream object and performs cryptographic + * transformation of the bytes read from the wrapped InputStream. It uses the cryptography handler + * provided during construction to invoke methods that perform the cryptographic transformations. + * + *

    + * In short, reading from the AwsCryptoInputStream returns bytes that are the cryptographic + * transformations of the bytes read from the wrapped InputStream. + * + *

    + * For example, if the cryptography handler provides methods for decryption, the AwsCryptoInputStream + * will read ciphertext bytes from the wrapped InputStream, decrypt, and return them as plaintext + * bytes. + * + *

    + * This class adheres strictly to the semantics, especially the failure semantics, of its ancestor + * class java.io.InputStream. This class overrides all the methods specified in its ancestor class. + * + *

    + * To instantiate an instance of this class, please see {@link AwsCrypto}. + * + */ +public class AwsCryptoInputStream extends InputStream { + + private final CryptoInputStream cryptoInputStream; + + /** + * Constructs an AwsCryptoInputStream that wraps the provided InputStream object. It performs + * cryptographic transformation of the bytes read from the wrapped InputStream using the methods + * provided in the provided CryptoHandler implementation. + * + * @param inputStream + * the inputStream object to be wrapped. + * @param cryptoHandler + * the cryptoHandler implementation that provides the methods to use in performing + * cryptographic transformation of the bytes read from the inputStream. + */ + AwsCryptoInputStream(final InputStream inputStream, final MessageCryptoHandler cryptoHandler) { + cryptoInputStream = new CryptoInputStream<>(inputStream, cryptoHandler); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public int read(final byte[] b, final int off, final int len) throws IllegalArgumentException, IOException, + BadCiphertextException { + return cryptoInputStream.read(b, off, len); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public int read(final byte[] b) throws IllegalArgumentException, IOException, BadCiphertextException { + return cryptoInputStream.read(b); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * if b contains invalid or corrupt ciphertext. This is thrown only during + * decryption. + */ + @Override + public int read() throws IOException, BadCiphertextException { + return cryptoInputStream.read(); + } + + @Override + public void close() throws IOException { + cryptoInputStream.close(); + } + + /** + * Returns metadata associated with the performed cryptographic operation. + */ + @Override + public int available() throws IOException { + return cryptoInputStream.available(); + } + + /** + * Sets an upper bound on the size of the input data. This method should be called before reading any data from the + * stream. If this method is not called prior to reading any data, performance may be reduced (notably, it will not + * be possible to cache data keys when encrypting). + * + * Among other things, this size is used to enforce limits configured on the {@link CachingCryptoMaterialsManager}. + * + * If the input size set here is exceeded, an exception will be thrown, and the encryption or decryption will fail. + * + * @param size Maximum input size. + */ + public void setMaxInputLength(long size) { + cryptoInputStream.setMaxInputLength(size); + } + + /** + * Gets the {@link AwsCryptoResult}. + * + * @return The {@link AwsCryptoResult} + * @throws IOException if an input/output exception occurs while processing the result + */ + public AwsCryptoResult getAwsCryptoResult() throws IOException { + return cryptoInputStream.getAwsCryptoResult(this); + } + + CryptoInputStream toCryptoInputStream() { + return cryptoInputStream; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoOutputStream.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoOutputStream.java new file mode 100644 index 000000000..13e5f3a2e --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoOutputStream.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An AwsCryptoOutputStream is a subclass of java.io.OutputStream. It performs cryptographic + * transformation of the bytes passing through it. + * + *

    + * The AwsCryptoOutputStream wraps a provided OutputStream object and performs cryptographic + * transformation of the bytes written to it. The transformed bytes are then written to the wrapped + * OutputStream. It uses the cryptography handler provided during construction to invoke methods + * that perform the cryptographic transformations. + * + *

    + * In short, writing to the AwsCryptoOutputStream results in those bytes being cryptographically + * transformed and written to the wrapped OutputStream. + * + *

    + * For example, if the crypto handler provides methods for decryption, the AwsCryptoOutputStream will + * decrypt the provided ciphertext bytes and write the plaintext bytes to the wrapped OutputStream. + * + *

    + * This class adheres strictly to the semantics, especially the failure semantics, of its ancestor + * class java.io.OutputStream. This class overrides all the methods specified in its ancestor class. + * + *

    + * To instantiate an instance of this class, please see {@link AwsCrypto}. + * + */ +public class AwsCryptoOutputStream extends OutputStream { + + private final CryptoOutputStream cryptoOutputStream; + + /** + * Constructs an AwsCryptoOutputStream that wraps the provided OutputStream object. It performs + * cryptographic transformation of the bytes written to it using the methods provided in the + * provided CryptoHandler implementation. The transformed bytes are then written to the wrapped + * OutputStream. + * + * @param outputStream + * the outputStream object to be wrapped. + * @param cryptoHandler + * the cryptoHandler implementation that provides the methods to use in performing + * cryptographic transformation of the bytes written to this stream. + */ + AwsCryptoOutputStream(final OutputStream outputStream, final MessageCryptoHandler cryptoHandler) { + cryptoOutputStream = new CryptoOutputStream<>(outputStream, cryptoHandler); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public void write(final byte[] b) throws IllegalArgumentException, IOException, BadCiphertextException { + cryptoOutputStream.write(b); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public void write(final byte[] b, final int off, final int len) throws IllegalArgumentException, IOException, + BadCiphertextException { + cryptoOutputStream.write(b, off, len); + } + + /** + * {@inheritDoc} + * + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid or corrupt + * ciphertext. + */ + @Override + public void write(int b) throws IOException, BadCiphertextException { + cryptoOutputStream.write(b); + } + + /** + * Closes this output stream and releases any system resources associated + * with this stream. + * + *

    + * This method writes any final bytes to the underlying stream that complete + * the cryptographic transformation of the written bytes. It also calls close + * on the wrapped OutputStream. + * + * @throws IOException + * if an I/O error occurs. + * @throws BadCiphertextException + * This is thrown only during decryption if b contains invalid + * or corrupt ciphertext. + */ + @Override + public void close() throws IOException, BadCiphertextException { + cryptoOutputStream.close(); + } + + /** + * Sets an upper bound on the size of the input data. This method should be called before writing any data to the + * stream. If this method is not called prior to writing data, performance may be reduced (notably, it will not + * be possible to cache data keys when encrypting). + * + * Among other things, this size is used to enforce limits configured on the {@link CachingCryptoMaterialsManager}. + * + * If the size set here is exceeded, an exception will be thrown, and the encryption or decryption will fail. + * + * @param size Maximum input size. + */ + public void setMaxInputLength(long size) { + cryptoOutputStream.setMaxInputLength(size); + } + + /** + * Gets the {@link AwsCryptoResult}. + * + * @return The {@link AwsCryptoResult} + */ + public AwsCryptoResult getAwsCryptoResult() { + return cryptoOutputStream.getAwsCryptoResult(this); + } + + CryptoOutputStream toCryptoOutputStream() { + return cryptoOutputStream; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoRequest.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoRequest.java new file mode 100644 index 000000000..658c044ad --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoRequest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.keyrings.Keyring; + +import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +class AwsCryptoRequest { + private final CryptoMaterialsManager cryptoMaterialsManager; + + AwsCryptoRequest(Builder builder) { + isTrue(builder.cryptoMaterialsManager != null || builder.keyring != null, + "Either a cryptoMaterialsManager or keyring is required"); + isTrue(builder.cryptoMaterialsManager == null || builder.keyring == null, + "Only one of cryptoMaterialsManager or keyring may be specified"); + + this.cryptoMaterialsManager = builder.cryptoMaterialsManager == null ? + new DefaultCryptoMaterialsManager(builder.keyring) : builder.cryptoMaterialsManager; + } + + public CryptoMaterialsManager cryptoMaterialsManager() { + return cryptoMaterialsManager; + } + + abstract static class Builder> { + + private CryptoMaterialsManager cryptoMaterialsManager; + private Keyring keyring; + + /** + * Sets the {@link CryptoMaterialsManager}. Either a {@link CryptoMaterialsManager} or a + * {@link Keyring} is required. + * + * @param cryptoMaterialsManager The {@link CryptoMaterialsManager} + * @return The Builder, for method chaining + */ + public T cryptoMaterialsManager(CryptoMaterialsManager cryptoMaterialsManager) { + requireNonNull(cryptoMaterialsManager, "cryptoMaterialsManager is required"); + this.cryptoMaterialsManager = cryptoMaterialsManager; + return getThis(); + } + + /** + * Sets the {@link Keyring}. Either a {@link CryptoMaterialsManager} or a + * {@link Keyring} is required. + * + * @param keyring The {@link Keyring} + * @return The Builder, for method chaining + */ + public T keyring(Keyring keyring) { + requireNonNull(keyring, "keyring is required"); + this.keyring = keyring; + return getThis(); + } + + abstract T getThis(); + + T applyMutation(Consumer mutator) { + mutator.accept(getThis()); + return getThis(); + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoResult.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoResult.java new file mode 100644 index 000000000..6bd38b78f --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCryptoResult.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; +import com.amazonaws.encryptionsdk.model.CiphertextHeaders; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Represents the result of an operation by {@link AwsCrypto}. It not only captures the + * {@code result} of the operation but also additional metadata such as the + * {@code encryptionContext}, {@code algorithm}, {@link KeyringTrace}, and any other information + * captured in the {@link CiphertextHeaders}. + * + * @param + * the type of the underlying {@code result} + */ +public class AwsCryptoResult { + private final T result; + private final KeyringTrace keyringTrace; + private final List masterKeys; + private final Map encryptionContext; + private final CiphertextHeaders headers; + + /** + * Note, does not make a defensive copy of any of the data. + */ + AwsCryptoResult(final T result, final KeyringTrace keyringTrace, final List masterKeys, final CiphertextHeaders headers) { + this.result = result; + this.keyringTrace = keyringTrace; + this.masterKeys = Collections.unmodifiableList(masterKeys); + this.headers = headers; + encryptionContext = this.headers.getEncryptionContextMap(); + } + + /** + * The actual result of the cryptographic operation. This is not a defensive copy and callers + * should not modify it. + * + * @return The result + */ + public T getResult() { + return result; + } + + /** + * The {@link KeyringTrace} containing all of the actions that keyrings have taken. + * + * @return The {@link KeyringTrace} + */ + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + /** + * The encryption context. + * + * @return The encryption context + */ + public Map getEncryptionContext() { + return encryptionContext; + } + + /** + * Convenience method equivalent to {@link #getHeaders()}{@code .getCryptoAlgoId()}. + */ + public CryptoAlgorithm getAlgorithmSuite() { + return headers.getCryptoAlgoId(); + } + + /** + * The Ciphertext Headers. + * + * @return The CiphertextHeaders + */ + public CiphertextHeaders getHeaders() { + return headers; + } + + /** + * Package-private method for converting to a legacy CryptoResult. + */ + CryptoResult toCryptoResult() { + return new CryptoResult<>(result, masterKeys, headers); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingInputStreamRequest.java b/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingInputStreamRequest.java new file mode 100644 index 000000000..989612f0b --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingInputStreamRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import java.io.InputStream; + +import static java.util.Objects.requireNonNull; + +public class CreateDecryptingInputStreamRequest extends AwsCryptoRequest { + + private final InputStream inputStream; + + private CreateDecryptingInputStreamRequest(Builder builder) { + super(builder); + + requireNonNull(builder.inputStream, "inputStream is required"); + this.inputStream = builder.inputStream; + } + + /** + * The {@link InputStream} to be read from. + * + * @return The {@link InputStream} to be read from. + */ + public InputStream inputStream() { + return this.inputStream; + } + + /** + * A builder for constructing an instance of {@code CreateDecryptingInputStreamRequest}. + * + * @return A builder for constructing an instance of {@code CreateDecryptingInputStreamRequest}. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private InputStream inputStream; + + /** + * Sets the {@link InputStream} + * + * @param inputStream The {@link InputStream} + * @return The Builder, for method chaining + */ + public Builder inputStream(InputStream inputStream) { + requireNonNull(inputStream, "inputStream is required"); + this.inputStream = inputStream; + return this; + } + + /** + * Constructs the CreateEncryptingInputStreamRequest instance. + * + * @return The CreateEncryptingInputStreamRequest instance + */ + public CreateDecryptingInputStreamRequest build() { + return new CreateDecryptingInputStreamRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingOutputStreamRequest.java b/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingOutputStreamRequest.java new file mode 100644 index 000000000..372e9c1b9 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/CreateDecryptingOutputStreamRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import java.io.OutputStream; + +import static java.util.Objects.requireNonNull; + +public class CreateDecryptingOutputStreamRequest extends AwsCryptoRequest { + + private final OutputStream outputStream; + + private CreateDecryptingOutputStreamRequest(Builder builder) { + super(builder); + + requireNonNull(builder.outputStream, "outputStream is required"); + this.outputStream = builder.outputStream; + } + + /** + * The {@link OutputStream} to be read from. + * + * @return The {@link OutputStream} to be read from. + */ + public OutputStream outputStream() { + return this.outputStream; + } + + /** + * A builder for constructing an instance of {@code CreateDecryptingOutputStreamRequest}. + * + * @return A builder for constructing an instance of {@code CreateDecryptingOutputStreamRequest}. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private OutputStream outputStream; + + /** + * Sets the {@link OutputStream} + * + * @param outputStream The {@link OutputStream} + * @return The Builder, for method chaining + */ + public Builder outputStream(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream is required"); + this.outputStream = outputStream; + return this; + } + + /** + * Constructs the CreateDecryptingOutputStreamRequest instance. + * + * @return The CreateDecryptingOutputStreamRequest instance + */ + public CreateDecryptingOutputStreamRequest build() { + return new CreateDecryptingOutputStreamRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingInputStreamRequest.java b/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingInputStreamRequest.java new file mode 100644 index 000000000..98926977b --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingInputStreamRequest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class CreateEncryptingInputStreamRequest extends AwsCryptoRequest { + + private final InputStream inputStream; + private final Map encryptionContext; + + private CreateEncryptingInputStreamRequest(Builder builder) { + super(builder); + + requireNonNull(builder.inputStream, "inputStream is required"); + requireNonNull(builder.encryptionContext, "encryptionContext is required"); + this.inputStream = builder.inputStream; + this.encryptionContext = builder.encryptionContext; + } + + /** + * The {@link InputStream} to be read from. + * + * @return The {@link InputStream} to be read from. + */ + public InputStream inputStream() { + return this.inputStream; + } + + /** + * The encryption context associated with this encryption. + * + * @return The encryption context associated with this encryption. + */ + public Map encryptionContext() { + return this.encryptionContext; + } + + /** + * A builder for constructing an instance of {@code CreateEncryptingInputStreamRequest}. + * + * @return A builder for constructing an instance of {@code CreateEncryptingInputStreamRequest}. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private InputStream inputStream; + private Map encryptionContext = Collections.emptyMap(); + + /** + * Sets the {@link InputStream} + * + * @param inputStream The {@link InputStream} + * @return The Builder, for method chaining + */ + public Builder inputStream(InputStream inputStream) { + requireNonNull(inputStream, "inputStream is required"); + this.inputStream = inputStream; + return this; + } + + /** + * Sets the (optional) encryption context map. + * + * @param encryptionContext The encryption context + * @return The Builder, for method chaining + */ + public Builder encryptionContext(Map encryptionContext) { + requireNonNull(encryptionContext, "encryptionContext is required"); + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + /** + * Constructs the CreateEncryptingInputStreamRequest instance. + * + * @return The CreateEncryptingInputStreamRequest instance + */ + public CreateEncryptingInputStreamRequest build() { + return new CreateEncryptingInputStreamRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingOutputStreamRequest.java b/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingOutputStreamRequest.java new file mode 100644 index 000000000..bb76b78a8 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/CreateEncryptingOutputStreamRequest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class CreateEncryptingOutputStreamRequest extends AwsCryptoRequest { + + private final OutputStream outputStream; + private final Map encryptionContext; + + private CreateEncryptingOutputStreamRequest(Builder builder) { + super(builder); + + requireNonNull(builder.outputStream, "outputStream is required"); + requireNonNull(builder.encryptionContext, "encryptionContext is required"); + this.outputStream = builder.outputStream; + this.encryptionContext = builder.encryptionContext; + } + + /** + * The {@link OutputStream} to be read from. + * + * @return The {@link OutputStream} to be read from. + */ + public OutputStream outputStream() { + return this.outputStream; + } + + /** + * The encryption context associated with this encryption. + * + * @return The encryption context associated with this encryption. + */ + public Map encryptionContext() { + return this.encryptionContext; + } + + /** + * A builder for constructing an instance of {@code CreateEncryptingOutputStreamRequest}. + * + * @return A builder for constructing an instance of {@code CreateEncryptingOutputStreamRequest}. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private OutputStream outputStream; + private Map encryptionContext = Collections.emptyMap(); + + /** + * Sets the {@link OutputStream} + * + * @param outputStream The {@link OutputStream} + * @return The Builder, for method chaining + */ + public Builder outputStream(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream is required"); + this.outputStream = outputStream; + return this; + } + + /** + * Sets the (optional) encryption context map. + * + * @param encryptionContext The encryption context + * @return The Builder, for method chaining + */ + public Builder encryptionContext(Map encryptionContext) { + requireNonNull(encryptionContext, "encryptionContext is required"); + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + /** + * Constructs the CreateEncryptingOutputStreamRequest instance. + * + * @return The CreateEncryptingOutputStreamRequest instance + */ + public CreateEncryptingOutputStreamRequest build() { + return new CreateEncryptingOutputStreamRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java index 935e4fbc0..ee1588deb 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java @@ -51,7 +51,10 @@ * * @param * The type of {@link MasterKey}s used to manipulate the data. + * + * @deprecated Replaced by {@link AwsCryptoInputStream} */ +@Deprecated public class CryptoInputStream> extends InputStream { private static final int MAX_READ_LEN = 4096; @@ -254,4 +257,18 @@ public CryptoResult, K> getCryptoResult() throws BadCiphert (List) cryptoHandler_.getMasterKeys(), cryptoHandler_.getHeaders()); } + + AwsCryptoResult getAwsCryptoResult(AwsCryptoInputStream awsCryptoInputStream) throws IOException { + while (!cryptoHandler_.getHeaders().isComplete()) { + if (fillOutBytes() == -1) { + throw new BadCiphertextException("No CiphertextHeaders found."); + } + } + + return new AwsCryptoResult<>( + awsCryptoInputStream, + cryptoHandler_.getKeyringTrace(), + cryptoHandler_.getMasterKeys(), + cryptoHandler_.getHeaders()); + } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java index de17f4a2f..6a4cda59e 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java @@ -49,7 +49,10 @@ * * @param * The type of {@link MasterKey}s used to manipulate the data. + * + * @deprecated Replaced by {@link AwsCryptoOutputStream} */ +@Deprecated public class CryptoOutputStream> extends OutputStream { private final OutputStream outputStream_; @@ -181,4 +184,16 @@ public CryptoResult, K> getCryptoResult() { (List) cryptoHandler_.getMasterKeys(), cryptoHandler_.getHeaders()); } -} \ No newline at end of file + + AwsCryptoResult getAwsCryptoResult(AwsCryptoOutputStream awsCryptoOutputStream) { + if (!cryptoHandler_.getHeaders().isComplete()) { + throw new IllegalStateException("Ciphertext headers not yet written to stream"); + } + + return new AwsCryptoResult<>( + awsCryptoOutputStream, + cryptoHandler_.getKeyringTrace(), + cryptoHandler_.getMasterKeys(), + cryptoHandler_.getHeaders()); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoResult.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoResult.java index 616ed3756..a550b4687 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoResult.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoResult.java @@ -30,7 +30,10 @@ * the type of the underlying {@code result} * @param * the type of the {@link MasterKey}s used in production of this result + * + * @deprecated Replaced by {@link AwsCryptoResult} */ +@Deprecated public class CryptoResult> { private final T result_; private final List masterKeys_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/DataKey.java b/src/main/java/com/amazonaws/encryptionsdk/DataKey.java index 21d174f00..a07287447 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/DataKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/DataKey.java @@ -20,7 +20,10 @@ * * @param * the type of {@link MasterKey} used to protect this {@code DataKey}. + * @deprecated This type depends on the deprecated {@link MasterKey} type. Use {@link SecretKey} or + * {@link EncryptedDataKey} instead. */ +@Deprecated public class DataKey> implements EncryptedDataKey { private final byte[] providerInformation_; private final byte[] encryptedDataKey_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/DecryptRequest.java b/src/main/java/com/amazonaws/encryptionsdk/DecryptRequest.java new file mode 100644 index 000000000..075df54cd --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/DecryptRequest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +public class DecryptRequest extends AwsCryptoRequest { + + private final ParsedCiphertext parsedCiphertext; + + private DecryptRequest(Builder builder) { + super(builder); + + isTrue(builder.ciphertext != null || builder.parsedCiphertext != null, + "Either ciphertext or parsedCiphertext is required"); + isTrue(builder.ciphertext == null || builder.parsedCiphertext == null, + "Only one of ciphertext or parsedCiphertext may be specified"); + + this.parsedCiphertext = builder.parsedCiphertext == null ? + new ParsedCiphertext(builder.ciphertext) : builder.parsedCiphertext; + } + + public ParsedCiphertext parsedCiphertext() { + return this.parsedCiphertext; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private ParsedCiphertext parsedCiphertext; + private byte[] ciphertext; + + /** + * Sets the {@link ParsedCiphertext} to decrypt. Either {@link ParsedCiphertext} or a + * {@code byte[]} ciphertext is required. + * + * @param parsedCiphertext The {@link ParsedCiphertext} + * @return The Builder, for method chaining + */ + public Builder parsedCiphertext(ParsedCiphertext parsedCiphertext) { + requireNonNull(parsedCiphertext, "parsedCiphertext is required"); + this.parsedCiphertext = parsedCiphertext; + return this; + } + + /** + * Sets the ciphertext byte array to decrypt. Either {@link ParsedCiphertext} or a + * {@code byte[]} ciphertext is required. Note that this does not make a defensive + * copy of the ciphertext and so any modifications made to the backing array will be + * reflected in this Builder. + * + * @param ciphertext The ciphertext + * @return The Builder, for method chaining + */ + public Builder ciphertext(byte[] ciphertext) { + requireNonNull(ciphertext, "ciphertext is required"); + this.ciphertext = ciphertext; + return this; + } + + /** + * Constructs the DecryptRequest instance. + * + * @return The DecryptRequest instance + */ + public DecryptRequest build() { + return new DecryptRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java b/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java index d31c615b5..781d193f5 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java +++ b/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java @@ -1,9 +1,11 @@ package com.amazonaws.encryptionsdk; -import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull; +import static com.amazonaws.encryptionsdk.AwsCrypto.getDefaultCryptoAlgorithm; +import static java.util.Objects.requireNonNull; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.HashMap; @@ -13,61 +15,91 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; import com.amazonaws.encryptionsdk.internal.Constants; -import com.amazonaws.encryptionsdk.internal.Utils; import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; +import com.amazonaws.encryptionsdk.keyrings.Keyring; 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 com.amazonaws.encryptionsdk.model.KeyBlob; -import com.amazonaws.encryptionsdk.model.EncryptionMaterials; /** * The default implementation of {@link CryptoMaterialsManager}, used implicitly when passing a - * {@link MasterKeyProvider} to methods in {@link AwsCrypto}. + * {@link MasterKeyProvider} or {@link Keyring} to methods in {@link AwsCrypto}. * - * This default implementation delegates to a specific {@link MasterKeyProvider} specified at construction time. It also - * handles generating trailing signature keys when needed, placing them in the encryption context (and extracting them - * at decrypt time). + * This default implementation delegates to a specific {@link MasterKeyProvider} or {@link Keyring} specified at + * construction time. It also handles generating trailing signature keys when needed, placing them in the + * encryption context (and extracting them at decrypt time). */ public class DefaultCryptoMaterialsManager implements CryptoMaterialsManager { + // Exactly one of keyring or mkp should be null + private final Keyring keyring; private final MasterKeyProvider mkp; /** - * @param mkp The master key provider to delegate to + * @param masterKeyProvider The master key provider to delegate to + * + * @deprecated Replaced by {@link #DefaultCryptoMaterialsManager(Keyring)} */ - public DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { - Utils.assertNonNull(mkp, "mkp"); - this.mkp = mkp; + @Deprecated + public DefaultCryptoMaterialsManager(MasterKeyProvider masterKeyProvider) { + requireNonNull(masterKeyProvider, "masterKeyProvider is required"); + this.mkp = masterKeyProvider; + this.keyring = null; } - @Override public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { - Map context = request.getContext(); + /** + * @param keyring The keyring to delegate to + */ + public DefaultCryptoMaterialsManager(Keyring keyring) { + requireNonNull(keyring, "keyring is required"); + this.keyring = keyring; + this.mkp = null; + } - CryptoAlgorithm algo = request.getRequestedAlgorithm(); - if (algo == null) { - algo = AwsCrypto.getDefaultCryptoAlgorithm(); + @Override + public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { + if(keyring != null) { + return getEncryptionMaterialsForKeyring(request); } - KeyPair trailingKeys = null; - if (algo.getTrailingSignatureLength() > 0) { - try { - trailingKeys = generateTrailingSigKeyPair(algo); - if (context.containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { - throw new IllegalArgumentException("EncryptionContext contains reserved field " - + Constants.EC_PUBLIC_KEY_FIELD); - } - // make mutable - context = new HashMap<>(context); - context.put(Constants.EC_PUBLIC_KEY_FIELD, serializeTrailingKeyForEc(algo, trailingKeys)); - } catch (final GeneralSecurityException ex) { - throw new AwsCryptoException(ex); - } + return getEncryptionMaterialsForMasterKeyProvider(request); + } + + @Override + public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { + if(keyring != null) { + return getDecryptionMaterialsForKeyring(request); } - final MasterKeyRequest.Builder mkRequestBuilder = MasterKeyRequest.newBuilder(); - mkRequestBuilder.setEncryptionContext(context); + return getDecryptionMaterialsForMasterKeyProvider(request); + } + + private EncryptionMaterials getEncryptionMaterialsForKeyring(EncryptionMaterialsRequest request) { + final CryptoAlgorithm algorithmSuite = request.getRequestedAlgorithm() != null ? + request.getRequestedAlgorithm() : getDefaultCryptoAlgorithm(); + final Map encryptionContext = new HashMap<>(request.getContext()); + final PrivateKey signingKey = getSigningKey(algorithmSuite, encryptionContext); + + final EncryptionMaterials encryptionMaterials = + EncryptionMaterials.newBuilder() + .setAlgorithm(algorithmSuite) + .setEncryptionContext(encryptionContext) + .setTrailingSignatureKey(signingKey) + .build(); - mkRequestBuilder.setStreaming(request.getPlaintextSize() == -1); + return keyring.onEncrypt(encryptionMaterials); + } + + private EncryptionMaterials getEncryptionMaterialsForMasterKeyProvider(EncryptionMaterialsRequest request) { + final Map encryptionContext = new HashMap<>(request.getContext()); + final CryptoAlgorithm algorithmSuite = request.getRequestedAlgorithm() != null ? + request.getRequestedAlgorithm() : getDefaultCryptoAlgorithm(); + final PrivateKey trailingSignatureKey = getSigningKey(algorithmSuite, encryptionContext); + + final MasterKeyRequest.Builder mkRequestBuilder = MasterKeyRequest.newBuilder() + .setEncryptionContext(encryptionContext) + .setStreaming(request.getPlaintextSize() == -1); if (request.getPlaintext() != null) { mkRequestBuilder.setPlaintext(request.getPlaintext()); } else { @@ -75,69 +107,102 @@ public DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { } @SuppressWarnings("unchecked") - final List mks - = (List)assertNonNull(mkp, "provider") - .getMasterKeysForEncryption(mkRequestBuilder.build()); + final List masterKeys = (List) mkp.getMasterKeysForEncryption(mkRequestBuilder.build()); - if (mks.isEmpty()) { + if (masterKeys.isEmpty()) { throw new IllegalArgumentException("No master keys provided"); } - DataKey dataKey = mks.get(0).generateDataKey(algo, context); + final DataKey dataKey = masterKeys.get(0).generateDataKey(algorithmSuite, encryptionContext); - List keyBlobs = new ArrayList<>(mks.size()); + final List keyBlobs = new ArrayList<>(masterKeys.size()); keyBlobs.add(new KeyBlob(dataKey)); - for (int i = 1; i < mks.size(); i++) { + for (int i = 1; i < masterKeys.size(); i++) { //noinspection unchecked - keyBlobs.add(new KeyBlob(mks.get(i).encryptDataKey(algo, context, dataKey))); + keyBlobs.add(new KeyBlob(masterKeys.get(i).encryptDataKey(algorithmSuite, encryptionContext, dataKey))); } - //noinspection unchecked return EncryptionMaterials.newBuilder() - .setAlgorithm(algo) - .setCleartextDataKey(dataKey.getKey()) - .setEncryptedDataKeys(keyBlobs) - .setEncryptionContext(context) - .setTrailingSignatureKey(trailingKeys == null ? null : trailingKeys.getPrivate()) - .setMasterKeys(mks) - .build(); + .setAlgorithm(algorithmSuite) + .setCleartextDataKey(dataKey.getKey()) + .setEncryptedDataKeys(keyBlobs) + .setEncryptionContext(encryptionContext) + .setTrailingSignatureKey(trailingSignatureKey) + .setMasterKeys(masterKeys) + .build(); } - @Override public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { - DataKey dataKey = mkp.decryptDataKey( + private DecryptionMaterials getDecryptionMaterialsForKeyring(DecryptionMaterialsRequest request) { + final PublicKey verificationKey = getVerificationKey(request); + + final DecryptionMaterials decryptionMaterials = + DecryptionMaterials.newBuilder() + .setAlgorithm(request.getAlgorithm()) + .setEncryptionContext(request.getEncryptionContext()) + .setTrailingSignatureKey(verificationKey) + .build(); + + final DecryptionMaterials result = keyring.onDecrypt(decryptionMaterials, request.getEncryptedDataKeys()); + + if(!result.hasCleartextDataKey()) { + throw new CannotUnwrapDataKeyException("Could not decrypt any data keys"); + } + + return result; + } + + private DecryptionMaterials getDecryptionMaterialsForMasterKeyProvider(DecryptionMaterialsRequest request) { + final DataKey dataKey = mkp.decryptDataKey( request.getAlgorithm(), request.getEncryptedDataKeys(), - request.getEncryptionContext() - ); + request.getEncryptionContext()); if (dataKey == null) { throw new CannotUnwrapDataKeyException("Could not decrypt any data keys"); } - PublicKey pubKey = null; + return DecryptionMaterials.newBuilder() + .setDataKey(dataKey) + .setTrailingSignatureKey(getVerificationKey(request)) + .build(); + } + + private PrivateKey getSigningKey(CryptoAlgorithm algorithmSuite, Map encryptionContext) { + if (algorithmSuite.getTrailingSignatureLength() > 0) { + try { + final KeyPair trailingKeys = generateTrailingSigKeyPair(algorithmSuite); + if (encryptionContext.containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { + throw new IllegalArgumentException("EncryptionContext contains reserved field " + + Constants.EC_PUBLIC_KEY_FIELD); + } + encryptionContext.put(Constants.EC_PUBLIC_KEY_FIELD, serializeTrailingKeyForEc(algorithmSuite, trailingKeys)); + return trailingKeys.getPrivate(); + } catch (final GeneralSecurityException ex) { + throw new AwsCryptoException(ex); + } + } + + return null; + } + + private PublicKey getVerificationKey(DecryptionMaterialsRequest request) { if (request.getAlgorithm().getTrailingSignatureLength() > 0) { try { - String serializedPubKey = request.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD); + final String serializedPubKey = request.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD); if (serializedPubKey == null) { throw new AwsCryptoException("Missing trailing signature public key"); } - pubKey = deserializeTrailingKeyFromEc(request.getAlgorithm(), serializedPubKey); + return TrailingSignatureAlgorithm.forCryptoAlgorithm( + request.getAlgorithm()).deserializePublicKey(serializedPubKey); } catch (final IllegalStateException ex) { throw new AwsCryptoException(ex); } } - return DecryptionMaterials.newBuilder() - .setDataKey(dataKey) - .setTrailingSignatureKey(pubKey) - .build(); - } - - private PublicKey deserializeTrailingKeyFromEc(CryptoAlgorithm algo, String pubKey) { - return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).deserializePublicKey(pubKey); + return null; } private static String serializeTrailingKeyForEc(CryptoAlgorithm algo, KeyPair trailingKeys) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/EncryptRequest.java b/src/main/java/com/amazonaws/encryptionsdk/EncryptRequest.java new file mode 100644 index 000000000..60890d802 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/EncryptRequest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.keyrings.Keyring; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class EncryptRequest extends AwsCryptoRequest { + + private final byte[] plaintext; + private final Map encryptionContext; + + private EncryptRequest(Builder builder) { + super(builder); + + requireNonNull(builder.plaintext, "plaintext is required"); + requireNonNull(builder.encryptionContext, "encryptionContext is required"); + this.plaintext = builder.plaintext; + this.encryptionContext = builder.encryptionContext; + } + + public byte[] plaintext() { + return this.plaintext; + } + + public Map encryptionContext() { + return this.encryptionContext; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private byte[] plaintext; + private Map encryptionContext = Collections.emptyMap(); + + /** + * Sets the plaintext byte array to encrypt. Note that this does not make a defensive copy of the + * plaintext and so any modifications made to the backing array will be reflected in this Builder. + * + * @param plaintext The {@link Keyring} + * @return The Builder, for method chaining + */ + public Builder plaintext(byte[] plaintext) { + requireNonNull(plaintext, "plaintext is required"); + this.plaintext = plaintext; + return this; + } + + /** + * Sets the (optional) encryption context map. + * + * @param encryptionContext The encryption context + * @return The Builder, for method chaining + */ + public Builder encryptionContext(Map encryptionContext) { + requireNonNull(encryptionContext, "encryptionContext is required"); + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + /** + * Constructs the EncryptRequest instance. + * + * @return The EncryptRequest instance + */ + public EncryptRequest build() { + return new EncryptRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java b/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java index 4629a9e07..aa5cfeb89 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java @@ -16,9 +16,13 @@ //@ model import java.util.Arrays; //@ model import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; //@ nullable_by_default public interface EncryptedDataKey { + + Charset PROVIDER_ENCODING = StandardCharsets.UTF_8; //@// An EncryptedDataKey object abstractly contains 3 pieces of data. //@// These are represented by 3 byte arrays: diff --git a/src/main/java/com/amazonaws/encryptionsdk/EstimateCiphertextSizeRequest.java b/src/main/java/com/amazonaws/encryptionsdk/EstimateCiphertextSizeRequest.java new file mode 100644 index 000000000..56b67380d --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/EstimateCiphertextSizeRequest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class EstimateCiphertextSizeRequest extends AwsCryptoRequest { + + private final int plaintextSize; + private final Map encryptionContext; + + private EstimateCiphertextSizeRequest(Builder builder) { + super(builder); + requireNonNull(builder.encryptionContext, "encryptionContext is required"); + + this.plaintextSize = builder.plaintextSize; + this.encryptionContext = builder.encryptionContext; + } + + public int plaintextSize() { + return plaintextSize; + } + + public Map encryptionContext() { + return encryptionContext; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AwsCryptoRequest.Builder { + + private int plaintextSize; + private Map encryptionContext = Collections.emptyMap(); + + /** + * Sets the plaintextSize. + * + * @param plaintextSize The plaintext size + * @return The Builder, for method chaining + */ + public Builder plaintextSize(int plaintextSize) { + this.plaintextSize = plaintextSize; + return this; + } + + /** + * Sets the (optional) encryption context map. + * + * @param encryptionContext The encryption context + * @return The Builder, for method chaining + */ + public Builder encryptionContext(Map encryptionContext) { + requireNonNull(encryptionContext, "encryptionContext is required"); + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + /** + * Constructs the EstimateCiphertextSizeRequest instance. + * + * @return The EstimateCiphertextSizeRequest instance + */ + public EstimateCiphertextSizeRequest build() { + return new EstimateCiphertextSizeRequest(this); + } + + @Override + Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/MasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/MasterKey.java index ae64e752e..9a247a463 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/MasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/MasterKey.java @@ -20,6 +20,7 @@ import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.keyrings.Keyring; /** * Represents the cryptographic key used to protect the {@link DataKey} (which, in turn, protects @@ -31,7 +32,10 @@ * * @param * the concrete type of the {@link MasterKey} + * + * @deprecated Replaced by {@link Keyring} */ +@Deprecated public abstract class MasterKey> extends MasterKeyProvider { public abstract String getProviderId(); diff --git a/src/main/java/com/amazonaws/encryptionsdk/MasterKeyProvider.java b/src/main/java/com/amazonaws/encryptionsdk/MasterKeyProvider.java index 978586f2e..216841d4e 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/MasterKeyProvider.java +++ b/src/main/java/com/amazonaws/encryptionsdk/MasterKeyProvider.java @@ -22,6 +22,7 @@ import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.keyrings.Keyring; /** * Represents the logic necessary to select and construct {@link MasterKey}s for encrypting and @@ -29,7 +30,10 @@ * * @param * the type of {@link MasterKey} returned by this provider + * + * @deprecated Replaced by {@link Keyring} */ +@Deprecated public abstract class MasterKeyProvider> { /** * ProviderId used by this instance when no other is specified. diff --git a/src/main/java/com/amazonaws/encryptionsdk/MasterKeyRequest.java b/src/main/java/com/amazonaws/encryptionsdk/MasterKeyRequest.java index 650fd2ee4..6dffe40b6 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/MasterKeyRequest.java +++ b/src/main/java/com/amazonaws/encryptionsdk/MasterKeyRequest.java @@ -17,12 +17,16 @@ import java.util.HashMap; import java.util.Map; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Contains information which {@link MasterKeyProvider}s can use to select which {@link MasterKey}s * to use to protect a given plaintext. This class is immutable. + * + * @deprecated MasterKeys and MasterKeyProviders have been deprecated in favor of {@link Keyring}s */ +@Deprecated public final class MasterKeyRequest { private final Map encryptionContext_; private final boolean isStreaming_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/caching/CachingCryptoMaterialsManager.java b/src/main/java/com/amazonaws/encryptionsdk/caching/CachingCryptoMaterialsManager.java index daf5c549e..28daf9e50 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/caching/CachingCryptoMaterialsManager.java +++ b/src/main/java/com/amazonaws/encryptionsdk/caching/CachingCryptoMaterialsManager.java @@ -14,6 +14,7 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer; import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest; import com.amazonaws.encryptionsdk.model.DecryptionMaterials; import com.amazonaws.encryptionsdk.model.EncryptionMaterials; @@ -22,16 +23,16 @@ /** * The CachingCryptoMaterialsManager wraps another {@link CryptoMaterialsManager}, and caches its results. This helps reduce - * the number of calls made to the underlying {@link CryptoMaterialsManager} and/or {@link MasterKeyProvider}, which may - * help reduce cost and/or improve performance. + * the number of calls made to the underlying {@link CryptoMaterialsManager}, {@link MasterKeyProvider} and/or + * {@link Keyring}, which may help reduce cost and/or improve performance. * * The CachingCryptoMaterialsManager helps enforce a number of usage limits on encrypt. Specifically, it limits the number of * individual messages encrypted with a particular data key, and the number of plaintext bytes encrypted with the same * data key. It also allows you to configure a maximum time-to-live for cache entries. * * Note that when performing streaming encryption operations, unless you set the stream size before writing any data - * using {@link com.amazonaws.encryptionsdk.CryptoOutputStream#setMaxInputLength(long)} or - * {@link com.amazonaws.encryptionsdk.CryptoInputStream#setMaxInputLength(long)}, the size of the message will not be + * using {@link com.amazonaws.encryptionsdk.AwsCryptoOutputStream#setMaxInputLength(long)} or + * {@link com.amazonaws.encryptionsdk.AwsCryptoInputStream#setMaxInputLength(long)}, the size of the message will not be * known, and to avoid exceeding byte use limits, caching will not be performed. * * By default, two different {@link CachingCryptoMaterialsManager}s will not share cached entries, even when using the same @@ -79,8 +80,8 @@ private Builder() {} * Sets the {@link CryptoMaterialsManager} that should be queried when the {@link CachingCryptoMaterialsManager} * incurs a cache miss. * - * You can set either a MasterKeyProvider or a CryptoMaterialsManager to back the CCMM - the last value set will - * be used. + * You can set either a MasterKeyProvider, a Keyring, or a CryptoMaterialsManager to back the CCMM - the last + * value set will be used. * * @param backingCMM The CryptoMaterialsManager to invoke on cache misses * @return this builder @@ -94,19 +95,39 @@ public Builder withBackingMaterialsManager(CryptoMaterialsManager backingCMM) { * Sets the {@link MasterKeyProvider} that should be queried when the {@link CachingCryptoMaterialsManager} * incurs a cache miss. * - * You can set either a MasterKeyProvider or a CryptoMaterialsManager to back the CCMM - the last value set will - * be used. + * You can set either a MasterKeyProvider, a Keyring, or a CryptoMaterialsManager to back the CCMM - the last + * value set will be used. * * This method is equivalent to calling {@link #withBackingMaterialsManager(CryptoMaterialsManager)} passing a * {@link DefaultCryptoMaterialsManager} constructed using your {@link MasterKeyProvider}. * + * @deprecated {@link MasterKeyProvider}s have been deprecated in favor of {@link Keyring}s. + * * @param mkp The MasterKeyProvider to invoke on cache misses * @return this builder */ + @Deprecated public Builder withMasterKeyProvider(MasterKeyProvider mkp) { return withBackingMaterialsManager(new DefaultCryptoMaterialsManager(mkp)); } + /** + * Sets the {@link Keyring} that should be queried when the {@link CachingCryptoMaterialsManager} + * incurs a cache miss. + * + * You can set either a MasterKeyProvider, a Keyring, or a CryptoMaterialsManager to back the CCMM - the last + * value set will be used. + * + * This method is equivalent to calling {@link #withBackingMaterialsManager(CryptoMaterialsManager)} passing a + * {@link DefaultCryptoMaterialsManager} constructed using your {@link Keyring}. + * + * @param keyring The Keyring to invoke on cache misses + * @return this builder + */ + public Builder withKeyring(Keyring keyring) { + return withBackingMaterialsManager(new DefaultCryptoMaterialsManager(keyring)); + } + /** * Sets the cache to which this {@link CryptoMaterialsManager} will be bound. * @param cache The cache to associate with the CMM diff --git a/src/main/java/com/amazonaws/encryptionsdk/exception/MalformedArnException.java b/src/main/java/com/amazonaws/encryptionsdk/exception/MalformedArnException.java new file mode 100644 index 000000000..58f78833c --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/exception/MalformedArnException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.exception; + +/** + * This exception is thrown when an Amazon Resource Name is provided that does not + * match the CMK Alias or ARN format. + */ +public class MalformedArnException extends AwsCryptoException { + + private static final long serialVersionUID = -1L; + + public MalformedArnException() { + super(); + } + + public MalformedArnException(final String message) { + super(message); + } + + public MalformedArnException(final Throwable cause) { + super(cause); + } + + public MalformedArnException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/exception/MismatchedDataKeyException.java b/src/main/java/com/amazonaws/encryptionsdk/exception/MismatchedDataKeyException.java new file mode 100644 index 000000000..46a062187 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/exception/MismatchedDataKeyException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.exception; + +/** + * This exception is thrown when the key used by KMS to decrypt a data key does not + * match the provider information contained within the encrypted data key. + */ +public class MismatchedDataKeyException extends AwsCryptoException { + + private static final long serialVersionUID = -1L; + + public MismatchedDataKeyException() { + super(); + } + + public MismatchedDataKeyException(final String message) { + super(message); + } + + public MismatchedDataKeyException(final Throwable cause) { + super(cause); + } + + public MismatchedDataKeyException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/exception/UnsupportedRegionException.java b/src/main/java/com/amazonaws/encryptionsdk/exception/UnsupportedRegionException.java new file mode 100644 index 000000000..1a3be9350 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/exception/UnsupportedRegionException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.exception; + +/** + * This exception is thrown when a region that is not allowed to be used by + * a given AwsKmsClientSupplier is specified. + */ +public class UnsupportedRegionException extends AwsCryptoException { + + private static final long serialVersionUID = -1L; + + public UnsupportedRegionException() { + super(); + } + + public UnsupportedRegionException(final String message) { + super(message); + } + + public UnsupportedRegionException(final Throwable cause) { + super(cause); + } + + public UnsupportedRegionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java index 7a4511f01..00f2cbbd1 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -32,7 +32,7 @@ class AesGcmJceKeyCipher extends JceKeyCipher { private static final int SPEC_LENGTH = Integer.BYTES + Integer.BYTES + NONCE_LENGTH; AesGcmJceKeyCipher(SecretKey key) { - super(key, key); + super(key, key, true); } private static byte[] specToBytes(final GCMParameterSpec spec) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/Constants.java b/src/main/java/com/amazonaws/encryptionsdk/internal/Constants.java index e580f7c79..684515d37 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/Constants.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/Constants.java @@ -62,4 +62,9 @@ private Constants() { public static final long MAX_FRAME_NUMBER = (1L << 32) - 1; public static final String EC_PUBLIC_KEY_FIELD = "aws-crypto-public-key"; + + /** + * The provider ID used for the AwsKmsKeyring + */ + public static final String AWS_KMS_PROVIDER_ID = "aws-kms"; } diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java index 386a4d867..38628d010 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java @@ -34,6 +34,8 @@ import com.amazonaws.encryptionsdk.MasterKeyProvider; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; import com.amazonaws.encryptionsdk.model.CiphertextFooters; import com.amazonaws.encryptionsdk.model.CiphertextHeaders; import com.amazonaws.encryptionsdk.model.CiphertextType; @@ -62,6 +64,7 @@ public class DecryptionHandler> implements MessageCryptoH private CryptoHandler contentCryptoHandler_; private DataKey dataKey_; + private KeyringTrace keyringTrace_; private SecretKey decryptionKey_; private CryptoAlgorithm cryptoAlgo_; private Signature trailingSig_; @@ -109,8 +112,11 @@ private DecryptionHandler(final CryptoMaterialsManager materialsManager, final C * the key blobs encoded in the provided ciphertext. * @throws AwsCryptoException * if the master key is null. + * @deprecated MasterKeyProviders have been deprecated in favor of {@link Keyring}s. + * Use {@link #create(CryptoMaterialsManager)} instead. */ @SuppressWarnings("unchecked") + @Deprecated public static > DecryptionHandler create( final MasterKeyProvider customerMasterKeyProvider ) throws AwsCryptoException { @@ -134,8 +140,11 @@ public static > DecryptionHandler create( * {@link #processBytes(byte[], int, int, byte[], int)} * @throws AwsCryptoException * if the master key is null. + * @deprecated MasterKeyProviders have been deprecated in favor of {@link Keyring}s. + * Use {@link #create(CryptoMaterialsManager, CiphertextHeaders)} instead. */ @SuppressWarnings("unchecked") + @Deprecated public static > DecryptionHandler create( final MasterKeyProvider customerMasterKeyProvider, final CiphertextHeaders headers ) throws AwsCryptoException { @@ -456,6 +465,7 @@ private void readHeaderFields(final CiphertextHeaders ciphertextHeaders) { //noinspection unchecked dataKey_ = (DataKey)result.getDataKey(); + keyringTrace_ = result.getKeyringTrace(); PublicKey trailingPublicKey = result.getTrailingSignatureKey(); try { @@ -534,11 +544,28 @@ public CiphertextHeaders getHeaders() { return ciphertextHeaders_; } + /** + * The master key that was used to decrypt the encrypted data key. This returns an + * empty list if Keyrings are in use. + * + * @deprecated MasterKeys have been deprecated in favor of {@link Keyring}s. + * Use {@link #getKeyringTrace()} to view which key was used in decryption. + */ @Override + @Deprecated public List getMasterKeys() { + if(dataKey_.getMasterKey() == null) { + return Collections.emptyList(); + } + return Collections.singletonList(dataKey_.getMasterKey()); } + @Override + public KeyringTrace getKeyringTrace() { + return keyringTrace_; + } + @Override public boolean isComplete() { return complete_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java index 843704a87..b2f18021a 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java @@ -21,12 +21,15 @@ import java.security.Signature; import java.security.SignatureException; import java.security.interfaces.ECPrivateKey; +import java.util.Collections; import java.util.List; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.SecretKey; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Sequence; @@ -58,6 +61,7 @@ public class EncryptionHandler implements MessageCryptoHandler { private final Map encryptionContext_; private final CryptoAlgorithm cryptoAlgo_; private final List masterKeys_; + private final KeyringTrace keyringTrace_; private final List keyBlobs_; private final SecretKey encryptionKey_; private final byte version_; @@ -90,6 +94,7 @@ public EncryptionHandler(int frameSize, EncryptionMaterials result) throws AwsCr this.encryptionContext_ = result.getEncryptionContext(); this.cryptoAlgo_ = result.getAlgorithm(); this.masterKeys_ = result.getMasterKeys(); + this.keyringTrace_ = result.getKeyringTrace(); this.keyBlobs_ = result.getEncryptedDataKeys(); this.trailingSignaturePrivateKey_ = result.getTrailingSignatureKey(); @@ -405,12 +410,28 @@ private CiphertextHeaders signCiphertextHeaders(final CiphertextHeaders unsigned return unsignedHeaders; } + /** + * The master keys that were used to encrypt the data key. This returns an + * empty list if Keyrings are in use. + * + * @deprecated MasterKeys have been deprecated in favor of {@link Keyring}s. + * Use {@link #getKeyringTrace()} to view which keys were used in encryption. + */ @Override + @Deprecated public List> getMasterKeys() { + if(masterKeys_ == null) { + return Collections.emptyList(); + } //noinspection unchecked return (List)masterKeys_; // This is unmodifiable } + @Override + public KeyringTrace getKeyringTrace() { + return keyringTrace_; + } + private void updateTrailingSignature(byte[] input, int offset, int len) { if (trailingDigest_ != null) { trailingDigest_.update(input, offset, len); diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index 643278a71..16aefda35 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -36,6 +36,7 @@ public abstract class JceKeyCipher { private final Key wrappingKey; private final Key unwrappingKey; private static final Charset KEY_NAME_ENCODING = StandardCharsets.UTF_8; + private final boolean encryptionContextSigned; /** * Returns a new instance of a JceKeyCipher based on the @@ -60,9 +61,10 @@ public static JceKeyCipher rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, return new RsaJceKeyCipher(wrappingKey, unwrappingKey, transformation); } - JceKeyCipher(Key wrappingKey, Key unwrappingKey) { + JceKeyCipher(Key wrappingKey, Key unwrappingKey, boolean encryptionContextSigned) { this.wrappingKey = wrappingKey; this.unwrappingKey = unwrappingKey; + this.encryptionContextSigned = encryptionContextSigned; } abstract WrappingData buildWrappingCipher(Key key, Map encryptionContext) throws GeneralSecurityException; @@ -70,6 +72,15 @@ public static JceKeyCipher rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, Map encryptionContext) throws GeneralSecurityException; + /** + * Returns true if this key cipher supports signing and verification + * of the encryption context. + * + * @return True if encryption context signing/verification is supported. + */ + public boolean isEncryptionContextSigned() { + return encryptionContextSigned; + } /** * Encrypts the given key, incorporating the given keyName and encryptionContext. diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/LazyMessageCryptoHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/LazyMessageCryptoHandler.java index bc50183a4..1d6dab5af 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/LazyMessageCryptoHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/LazyMessageCryptoHandler.java @@ -6,6 +6,7 @@ import java.util.function.Function; import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; import com.amazonaws.encryptionsdk.model.CiphertextHeaders; /** @@ -93,6 +94,11 @@ public List> getMasterKeys() { return getDelegate().getMasterKeys(); } + @Override + public KeyringTrace getKeyringTrace() { + return getDelegate().getKeyringTrace(); + } + @Override public int doFinal(byte[] out, int outOff) { return getDelegate().doFinal(out, outOff); diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/MessageCryptoHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/MessageCryptoHandler.java index 34445f867..1d2c7c93d 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/MessageCryptoHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/MessageCryptoHandler.java @@ -17,6 +17,8 @@ import java.util.Map; import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; import com.amazonaws.encryptionsdk.model.CiphertextHeaders; public interface MessageCryptoHandler extends CryptoHandler { @@ -48,6 +50,17 @@ public interface MessageCryptoHandler extends CryptoHandler { * All used {@link MasterKey}s. For encryption flows, these are all the * {@link MasterKey}s used to protect the data. In the decryption flow, it is the single * {@link MasterKey} actually used to decrypt the data. + * + * @deprecated MasterKeys and MasterKeyProviders have been deprecated in favor of {@link Keyring}s */ + @Deprecated List> getMasterKeys(); + + /** + * Gets the KeyringTrace containing all actions {@link Keyring}s have taken as part of + * encryption or decryption. + * + * @return the KeyringTrace + */ + KeyringTrace getKeyringTrace(); } diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java index c830f5487..47b65514a 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java @@ -44,7 +44,7 @@ class RsaJceKeyCipher extends JceKeyCipher { private final String transformation_; RsaJceKeyCipher(PublicKey wrappingKey, PrivateKey unwrappingKey, String transformation) { - super(wrappingKey, unwrappingKey); + super(wrappingKey, unwrappingKey, false); final Matcher matcher = SUPPORTED_TRANSFORMATIONS.matcher(transformation); if (matcher.matches()) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index 4995066cb..bf632356c 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -21,6 +21,7 @@ import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; import com.amazonaws.encryptionsdk.internal.JceKeyCipher; import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -37,7 +38,10 @@ * Represents a {@link MasterKey} backed by one (or more) JCE {@link Key}s. Instances of this should * only be acquired using {@link #getInstance(SecretKey, String, String, String)} or * {@link #getInstance(PublicKey, PrivateKey, String, String, String)}. + * + * @deprecated Replaced by {@code RawAesKeyring} and {@code RawRsaKeyring}. See {@link StandardKeyrings}. */ +@Deprecated public class JceMasterKey extends MasterKey { private final String providerName_; private final String keyId_; @@ -104,6 +108,16 @@ public String getKeyId() { return keyId_; } + /** + * Returns true if the underlying key cipher supports signing and + * verification of the encryption context. + * + * @return True if encryption context signing/verification is supported. + */ + public boolean isEncryptionContextSigned() { + return jceKeyCipher_.isEncryptionContextSigned(); + } + @Override public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/KeyStoreProvider.java b/src/main/java/com/amazonaws/encryptionsdk/jce/KeyStoreProvider.java index 1e92cd2bd..f220aea8b 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/KeyStoreProvider.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/KeyStoreProvider.java @@ -38,13 +38,17 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; /** * This {@link MasterKeyProvider} provides keys backed by a JCE {@link KeyStore}. Please see * {@link #decryptDataKey(CryptoAlgorithm, Collection, Map)} for an of how decryption is managed and * see {@link #getMasterKeysForEncryption(MasterKeyRequest)} for an explanation of how encryption is * managed. + * + * @deprecated Replaced by {@code RawAesKeyring} and {@code RawRsaKeyring}. See {@link StandardKeyrings}. */ +@Deprecated public class KeyStoreProvider extends MasterKeyProvider { private final String providerName_; private final KeyStore keystore_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyring.java new file mode 100644 index 000000000..76cd8a9b7 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyring.java @@ -0,0 +1,188 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao.DecryptDataKeyResult; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao.GenerateDataKeyResult; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static com.amazonaws.encryptionsdk.internal.Constants.AWS_KMS_PROVIDER_ID; +import static com.amazonaws.encryptionsdk.kms.AwsKmsCmkId.isKeyIdWellFormed; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; + +/** + * A keyring which interacts with AWS Key Management Service (KMS) to create, + * encrypt, and decrypt data keys using AWS KMS defined Customer Master Keys (CMKs). + */ +class AwsKmsKeyring implements Keyring { + + private final DataKeyEncryptionDao dataKeyEncryptionDao; + private final List keyIds; + private final AwsKmsCmkId generatorKeyId; + private final boolean isDiscovery; + + AwsKmsKeyring(DataKeyEncryptionDao dataKeyEncryptionDao, List keyIds, AwsKmsCmkId generatorKeyId, boolean isDiscovery) { + requireNonNull(dataKeyEncryptionDao, "dataKeyEncryptionDao is required"); + this.dataKeyEncryptionDao = dataKeyEncryptionDao; + this.keyIds = keyIds == null ? emptyList() : unmodifiableList(new ArrayList<>(keyIds)); + this.generatorKeyId = generatorKeyId; + this.isDiscovery = isDiscovery; + + if(isDiscovery) { + Validate.isTrue(generatorKeyId == null && this.keyIds.isEmpty(), + "AWS KMS Discovery keyrings cannot specify any key IDs"); + } else { + Validate.isTrue(generatorKeyId != null || !this.keyIds.isEmpty(), + "GeneratorKeyId or KeyIds are required for non-discovery AWS KMS Keyrings."); + } + + if (this.keyIds.contains(generatorKeyId)) { + throw new IllegalArgumentException("KeyIds should not contain the generatorKeyId"); + } + } + + @Override + public EncryptionMaterials onEncrypt(EncryptionMaterials encryptionMaterials) { + requireNonNull(encryptionMaterials, "encryptionMaterials are required"); + + // If this keyring is a discovery keyring, OnEncrypt MUST return the input encryption materials unmodified. + if (isDiscovery) { + return encryptionMaterials; + } + + EncryptionMaterials resultMaterials = encryptionMaterials; + + // If the input encryption materials do not contain a plaintext data key and this keyring does not + // have a generator defined, OnEncrypt MUST not modify the encryption materials and MUST fail. + if (!encryptionMaterials.hasCleartextDataKey() && generatorKeyId == null) { + throw new AwsCryptoException("Encryption materials must contain either a plaintext data key or a generator"); + } + + final List keyIdsToEncrypt = new ArrayList<>(keyIds); + + // If the input encryption materials do not contain a plaintext data key and a generator is defined onEncrypt + // MUST attempt to generate a new plaintext data key and encrypt that data key by calling KMS GenerateDataKey. + if (!encryptionMaterials.hasCleartextDataKey()) { + resultMaterials = generateDataKey(encryptionMaterials); + } else if (generatorKeyId != null) { + // If this keyring's generator is defined and was not used to generate a data key, OnEncrypt + // MUST also attempt to encrypt the plaintext data key using the CMK specified by the generator. + keyIdsToEncrypt.add(generatorKeyId); + } + + // Given a plaintext data key in the encryption materials, OnEncrypt MUST attempt + // to encrypt the plaintext data key using each CMK specified in it's key IDs list. + for (AwsKmsCmkId keyId : keyIdsToEncrypt) { + resultMaterials = encryptDataKey(keyId, resultMaterials); + } + + return resultMaterials; + } + + private EncryptionMaterials generateDataKey(final EncryptionMaterials encryptionMaterials) { + final GenerateDataKeyResult result = dataKeyEncryptionDao.generateDataKey(generatorKeyId, + encryptionMaterials.getAlgorithm(), encryptionMaterials.getEncryptionContext()); + + return encryptionMaterials + .withCleartextDataKey(result.getPlaintextDataKey(), + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, generatorKeyId.toString(), + KeyringTraceFlag.GENERATED_DATA_KEY)) + .withEncryptedDataKey(new KeyBlob(result.getEncryptedDataKey()), + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, generatorKeyId.toString(), + KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + } + + private EncryptionMaterials encryptDataKey(final AwsKmsCmkId keyId, final EncryptionMaterials encryptionMaterials) { + final EncryptedDataKey encryptedDataKey = dataKeyEncryptionDao.encryptDataKey(keyId, + encryptionMaterials.getCleartextDataKey(), encryptionMaterials.getEncryptionContext()); + + return encryptionMaterials.withEncryptedDataKey(new KeyBlob(encryptedDataKey), + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, keyId.toString(), + KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + } + + @Override + public DecryptionMaterials onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + requireNonNull(decryptionMaterials, "decryptionMaterials are required"); + requireNonNull(encryptedDataKeys, "encryptedDataKeys are required"); + + if (decryptionMaterials.hasCleartextDataKey() || encryptedDataKeys.isEmpty()) { + return decryptionMaterials; + } + + final Set configuredKeyIds = new HashSet<>(keyIds); + + if (generatorKeyId != null) { + configuredKeyIds.add(generatorKeyId); + } + + for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) { + if (okToDecrypt(encryptedDataKey, configuredKeyIds)) { + try { + final DecryptDataKeyResult result = dataKeyEncryptionDao.decryptDataKey(encryptedDataKey, + decryptionMaterials.getAlgorithm(), decryptionMaterials.getEncryptionContext()); + + return decryptionMaterials.withCleartextDataKey(result.getPlaintextDataKey(), + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, result.getKeyArn(), + KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT)); + } catch (CannotUnwrapDataKeyException e) { + continue; + } + } + } + + return decryptionMaterials; + } + + private boolean okToDecrypt(EncryptedDataKey encryptedDataKey, Set configuredKeyIds) { + // Only attempt to decrypt keys provided by KMS + if (!encryptedDataKey.getProviderId().equals(AWS_KMS_PROVIDER_ID)) { + return false; + } + + // If the key ID cannot be parsed, skip it + if(!isKeyIdWellFormed(new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING))) + { + return false; + } + + // If this keyring is a discovery keyring, OnDecrypt MUST attempt to + // decrypt every encrypted data key in the input encrypted data key list + if (isDiscovery) { + return true; + } + + // OnDecrypt MUST attempt to decrypt each input encrypted data key in the input + // encrypted data key list where the key provider info has a value equal to one + // of the ARNs in this keyring's key IDs or the generator + return configuredKeyIds.contains( + AwsKmsCmkId.fromString(new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING))); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringBuilder.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringBuilder.java new file mode 100644 index 000000000..5a67334e8 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringBuilder.java @@ -0,0 +1,122 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.kms.AwsKmsClientSupplier; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; + +import java.util.List; + +public class AwsKmsKeyringBuilder { + private AwsKmsClientSupplier awsKmsClientSupplier; + private List grantTokens; + private List keyIds; + private AwsKmsCmkId generatorKeyId; + private final boolean isDiscovery; + + private AwsKmsKeyringBuilder(boolean isDiscovery) { + // Use AwsKmsKeyringBuilder.standard() or StandardKeyrings.awsKmsBuilder() to instantiate + // a standard Aws Kms Keyring builder. If an Aws Kms Discovery Keyring builder is needed use + // AwsKmsKeyringBuilder.discovery() or StandardKeyrings.awsKmsDiscoveryBuilder(). + this.isDiscovery = isDiscovery; + } + + /** + * Constructs a new instance of {@code AwsKmsKeyringBuilder} + * + * @return The {@code AwsKmsKeyringBuilder} + */ + public static AwsKmsKeyringBuilder standard() { + return new AwsKmsKeyringBuilder(false); + } + + /** + * Constructs a new instance of {@code AwsKmsKeyringBuilder} that produces an AWS KMS Discovery keyring. + * AWS KMS Discovery keyrings do not specify any CMKs to decrypt with, and thus will attempt to decrypt + * using any encrypted data key in an encrypted message. AWS KMS Discovery keyrings do not perform encryption. + * + * @return The {@code AwsKmsKeyringBuilder} + */ + public static AwsKmsKeyringBuilder discovery() { + return new AwsKmsKeyringBuilder(true); + } + + /** + * A function that returns an AWS KMS client that can make GenerateDataKey, Encrypt, and Decrypt calls in + * a particular AWS region. If this is not supplied, the default AwsKmsClientSupplier will + * be used. AwsKmsClientSupplier.builder() can be used to construct this type. + * + * @param awsKmsClientSupplier The AWS KMS client supplier + * @return The AwsKmsKeyringBuilder, for method chaining + */ + public AwsKmsKeyringBuilder awsKmsClientSupplier(AwsKmsClientSupplier awsKmsClientSupplier) { + this.awsKmsClientSupplier = awsKmsClientSupplier; + return this; + } + + /** + * A list of string grant tokens to be included in all KMS calls. + * + * @param grantTokens The list of grant tokens + * @return The AwsKmsKeyringBuilder, for method chaining + */ + public AwsKmsKeyringBuilder grantTokens(List grantTokens) { + this.grantTokens = grantTokens; + return this; + } + + /** + * A list of {@link AwsKmsCmkId}s in ARN, CMK Alias, or ARN Alias format identifying AWS KMS CMKs + * used for encrypting and decrypting data keys. + * + * @param keyIds The list of AWS KMS CMKs + * @return The AwsKmsKeyringBuilder, for method chaining + */ + public AwsKmsKeyringBuilder keyIds(List keyIds) { + this.keyIds = keyIds; + return this; + } + + /** + * An {@link AwsKmsCmkId} in ARN, CMK Alias, or ARN Alias format that identifies a + * AWS KMS CMK responsible for generating a data key, as well as encrypting and + * decrypting data keys . + * + * @param generatorKeyId An {@link AwsKmsCmkId} in ARN, CMK Alias, or ARN Alias format that identifies a + * AWS KMS CMK responsible for generating a data key, as well as encrypting and + * decrypting data keys. + * @return The AwsKmsKeyringBuilder, for method chaining + */ + public AwsKmsKeyringBuilder generatorKeyId(AwsKmsCmkId generatorKeyId) { + this.generatorKeyId = generatorKeyId; + return this; + } + + /** + * Constructs the {@link Keyring} instance. + * + * @return The {@link Keyring} instance + */ + public Keyring build() { + if (awsKmsClientSupplier == null) { + awsKmsClientSupplier = StandardAwsKmsClientSuppliers.defaultBuilder().build(); + } + + return new AwsKmsKeyring(DataKeyEncryptionDao.awsKms(awsKmsClientSupplier, grantTokens), + keyIds, generatorKeyId, isDiscovery); + } + +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java new file mode 100644 index 000000000..0930b8406 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; + +import java.util.List; + +/** + * Keyrings are responsible for the generation, encryption, and decryption of data keys. + */ +public interface Keyring { + + /** + * Attempt to encrypt either the given data key (if present) or one that may be generated + * + * @param encryptionMaterials Materials needed for encryption. + * @return Encryption materials with added information provided by this keyring. + */ + EncryptionMaterials onEncrypt(EncryptionMaterials encryptionMaterials); + + /** + * Attempt to decrypt the encrypted data keys + * + * @param decryptionMaterials Materials needed for decryption. + * @param encryptedDataKeys List of encrypted data keys. + * @return Decryption materials with added information provided by this keyring. + */ + DecryptionMaterials onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys); + +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java new file mode 100644 index 000000000..9298194e6 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + +/** + * A keyring trace containing all of the actions that keyrings have taken on a set of encryption materials. + */ +public final class KeyringTrace { + + private final List entries; + public static final KeyringTrace EMPTY_TRACE = new KeyringTrace(emptyList()); + + public KeyringTrace(final List entries) { + this.entries = unmodifiableList(new ArrayList<>(entries)); + } + + /** + * Creates a new instance of {@code KeyringTrace} with the provided {@link KeyringTraceEntry}. + * + * @param entry The entry to include in the new {@code KeyringTrace}. + * @return The new {@code KeyringTrace} instance. + */ + public KeyringTrace with(KeyringTraceEntry entry) { + final List newEntries = new ArrayList<>(entries); + newEntries.add(entry); + return new KeyringTrace(newEntries); + } + + /** + * Gets an unmodifiable list of `KeyringTraceEntry`s ordered sequentially + * according to the order the actions were taken, with the earliest action + * corresponding to the first `KeyringTraceEntry` in the list. + * + * @return An unmodifiable list of `KeyringTraceEntry`s + */ + public List getEntries() { + return entries; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("entries", entries) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeyringTrace that = (KeyringTrace) o; + return Objects.equals(entries, that.entries); + } + + @Override + public int hashCode() { + return Objects.hash(entries); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java new file mode 100644 index 000000000..4f656c7be --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.apache.commons.lang3.Validate.notBlank; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * A representation of an action that a keyring has taken on a data key. + */ +public class KeyringTraceEntry { + + private final String keyNamespace; + private final String keyName; + private final Set flags; + + /** + * Constructs a new `KeyringTraceEntry`. + * + * @param keyNamespace The namespace for the key. + * @param keyName The name of the key. + * @param flags One or more KeyringTraceFlags + * indicating what actions were taken by a keyring. + */ + public KeyringTraceEntry(final String keyNamespace, final String keyName, final KeyringTraceFlag... flags) { + notBlank(keyNamespace, "keyNamespace is required"); + notBlank(keyName, "keyName is required"); + notEmpty(flags, "At least one flag is required"); + + this.keyNamespace = keyNamespace; + this.keyName = keyName; + this.flags = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(flags))); + } + + /** + * Returns the key namespace. + * + * @return The key namespace. + */ + public String getKeyNamespace() { + return this.keyNamespace; + } + + /** + * Returns the key name. + * + * @return The key name. + */ + public String getKeyName() { + return this.keyName; + } + + /** + * Returns an unmodifiable set of flags that indicate + * which actions were taken by a keyring. + * + * @return The unmodifiable set of flags. + */ + public Set getFlags() { + return this.flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeyringTraceEntry that = (KeyringTraceEntry) o; + return Objects.equals(keyNamespace, that.keyNamespace) && + Objects.equals(keyName, that.keyName) && + Objects.equals(flags, that.flags); + } + + @Override + public int hashCode() { + return Objects.hash(keyNamespace, keyName, flags); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("keyNamespace", this.keyNamespace) + .append("keyName", this.keyName) + .append("flags", this.flags) + .toString(); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceFlag.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceFlag.java new file mode 100644 index 000000000..94e59bc41 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceFlag.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +/** + * Enum representing the possible actions a keyring may take on the + * different wrapping keys it manages. + */ +public enum KeyringTraceFlag { + + /** + * A flag to represent that a keyring has generated a plaintext data key. + */ + GENERATED_DATA_KEY, + + /** + * A flag to represent that a keyring has created an encrypted data key. + */ + ENCRYPTED_DATA_KEY, + + /** + * A flag to represent that a keyring has obtained the + * corresponding plaintext data key from an encrypted data key. + */ + DECRYPTED_DATA_KEY, + + /** + * A flag to represent that the keyring has cryptographically + * bound the encryption context to a newly created encrypted data key. + */ + SIGNED_ENCRYPTION_CONTEXT, + + /** + * A flag to represent that the keyring has verified that an encrypted + * data key was originally created with a particular encryption context. + */ + VERIFIED_ENCRYPTION_CONTEXT +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java new file mode 100644 index 000000000..de00c7c85 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +/** + * A keyring which combines other keyrings, allowing one OnEncrypt or OnDecrypt call to + * modify the encryption or decryption materials using more than one keyring. + */ +class MultiKeyring implements Keyring { + + final Keyring generatorKeyring; + final List childrenKeyrings; + + MultiKeyring(Keyring generatorKeyring, List childrenKeyrings) { + this.generatorKeyring = generatorKeyring; + this.childrenKeyrings = childrenKeyrings == null ? emptyList() : unmodifiableList(new ArrayList<>(childrenKeyrings)); + + isTrue(this.generatorKeyring != null || !this.childrenKeyrings.isEmpty(), + "At least a generator keyring or children keyrings must be defined"); + } + + @Override + public EncryptionMaterials onEncrypt(EncryptionMaterials encryptionMaterials) { + requireNonNull(encryptionMaterials, "encryptionMaterials are required"); + + EncryptionMaterials resultMaterials = encryptionMaterials; + + if (generatorKeyring != null) { + resultMaterials = generatorKeyring.onEncrypt(encryptionMaterials); + } + + if (!resultMaterials.hasCleartextDataKey()) { + throw new AwsCryptoException("Either a generator keyring must be supplied that produces a cleartext " + + "data key or a cleartext data key must already be present in the encryption materials."); + } + + for (Keyring keyring : childrenKeyrings) { + resultMaterials = keyring.onEncrypt(resultMaterials); + } + + return resultMaterials; + } + + @Override + public DecryptionMaterials onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + requireNonNull(decryptionMaterials, "decryptionMaterials are required"); + requireNonNull(encryptedDataKeys, "encryptedDataKeys are required"); + + if (decryptionMaterials.hasCleartextDataKey()) { + return decryptionMaterials; + } + + final List keyringsToDecryptWith = new ArrayList<>(); + + if (generatorKeyring != null) { + keyringsToDecryptWith.add(generatorKeyring); + } + + keyringsToDecryptWith.addAll(childrenKeyrings); + + final List exceptions = new ArrayList<>(); + + for (Keyring keyring : keyringsToDecryptWith) { + try { + final DecryptionMaterials resultMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + if (resultMaterials.hasCleartextDataKey()) { + // Decryption succeeded, return immediately + return resultMaterials; + } + } catch (Exception e) { + exceptions.add(e); + } + } + + if (!exceptions.isEmpty()) { + final AwsCryptoException exception = new CannotUnwrapDataKeyException( + "Unable to decrypt data key and one or more child keyrings had an error.", exceptions.get(0)); + exceptions.forEach(exception::addSuppressed); + throw exception; + } + + return decryptionMaterials; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java new file mode 100644 index 000000000..1503bdd88 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.internal.Utils; + +import javax.crypto.SecretKey; + +/** + * A {@code Keyring} which does local AES-GCM encryption + * decryption of data keys using the provided wrapping key. + *

    + * Instantiate by using the {@code StandardKeyrings.rawAesBuilder(...)} factory method. + */ +class RawAesKeyring extends RawKeyring { + + RawAesKeyring(String keyNamespace, String keyName, SecretKey wrappingKey) { + super(keyNamespace, keyName, JceKeyCipher.aesGcm(wrappingKey)); + } + + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + + // the key provider ID of the encrypted data key must + // have a value equal to this keyring's key namespace. + if (!keyNamespace.equals(encryptedDataKey.getProviderId())) { + return false; + } + + // the key name obtained from the encrypted data key's key provider + // information must have a value equal to this keyring's key name. + if (!Utils.arrayPrefixEquals(encryptedDataKey.getProviderInformation(), keyNameBytes, keyNameBytes.length)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringBuilder.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringBuilder.java new file mode 100644 index 000000000..8f8929419 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringBuilder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import javax.crypto.SecretKey; + +public class RawAesKeyringBuilder { + private String keyNamespace; + private String keyName; + private SecretKey wrappingKey; + + private RawAesKeyringBuilder() { + // Use RawAesKeyringBuilder.standard() or StandardKeyrings.rawAes() to instantiate + } + + /** + * Constructs a new instance of {@code RawAesKeyringBuilder} + * + * @return The {@code RawAesKeyringBuilder} + */ + public static RawAesKeyringBuilder standard() { + return new RawAesKeyringBuilder(); + } + + /** + * A value that, together with the key name, identifies the wrapping key (required). + * + * @param keyNamespace The key namespace + * @return The RawAesKeyringBuilder, for method chaining + */ + public RawAesKeyringBuilder keyNamespace(String keyNamespace) { + this.keyNamespace = keyNamespace; + return this; + } + + /** + * A value that, together with the key namespace, identifies the wrapping key (required). + * + * @param keyName The key name + * @return The RawAesKeyringBuilder, for method chaining + */ + public RawAesKeyringBuilder keyName(String keyName) { + this.keyName = keyName; + return this; + } + + /** + * The AES key input to AES-GCM to encrypt plaintext data keys (required). + * + * @param wrappingKey The wrapping key + * @return The RawAesKeyringBuilder, for method chaining + */ + public RawAesKeyringBuilder wrappingKey(SecretKey wrappingKey) { + this.wrappingKey = wrappingKey; + return this; + } + + /** + * Constructs the {@link Keyring} instance. + * + * @return The {@link Keyring} instance + */ + public Keyring build() { + return new RawAesKeyring(keyNamespace, keyName, wrappingKey); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java new file mode 100644 index 000000000..f26604840 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.List; +import java.util.logging.Logger; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.DECRYPTED_DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.ENCRYPTED_DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.GENERATED_DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.notBlank; + +/** + * A keyring supporting local encryption and decryption using either RSA or AES-GCM. + */ +abstract class RawKeyring implements Keyring { + + final String keyNamespace; + final String keyName; + final byte[] keyNameBytes; + private final JceKeyCipher jceKeyCipher; + private static final Logger LOGGER = Logger.getLogger(RawKeyring.class.getName()); + + RawKeyring(final String keyNamespace, final String keyName, JceKeyCipher jceKeyCipher) { + notBlank(keyNamespace, "keyNamespace is required"); + notBlank(keyName, "keyName is required"); + requireNonNull(jceKeyCipher, "jceKeyCipher is required"); + + this.keyNamespace = keyNamespace; + this.keyName = keyName; + this.keyNameBytes = keyName.getBytes(PROVIDER_ENCODING); + this.jceKeyCipher = jceKeyCipher; + } + + /** + * Returns true if the given encrypted data key may be decrypted with this keyring. + * + * @param encryptedDataKey The encrypted data key. + * @return True if the key may be decrypted, false otherwise. + */ + abstract boolean validToDecrypt(EncryptedDataKey encryptedDataKey); + + @Override + public EncryptionMaterials onEncrypt(EncryptionMaterials encryptionMaterials) { + requireNonNull(encryptionMaterials, "encryptionMaterials are required"); + + EncryptionMaterials resultMaterials = encryptionMaterials; + + if (!encryptionMaterials.hasCleartextDataKey()) { + resultMaterials = generateDataKey(encryptionMaterials); + } + + final EncryptedDataKey encryptedDataKey = jceKeyCipher.encryptKey( + resultMaterials.getCleartextDataKey().getEncoded(), + keyName, keyNamespace, resultMaterials.getEncryptionContext()); + return resultMaterials.withEncryptedDataKey(new KeyBlob(encryptedDataKey), + new KeyringTraceEntry(keyNamespace, keyName, encryptTraceFlags())); + } + + @Override + public DecryptionMaterials onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + requireNonNull(decryptionMaterials, "decryptionMaterials are required"); + requireNonNull(encryptedDataKeys, "encryptedDataKeys are required"); + + if (decryptionMaterials.hasCleartextDataKey() || encryptedDataKeys.isEmpty()) { + return decryptionMaterials; + } + + for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) { + if (validToDecrypt(encryptedDataKey)) { + try { + final byte[] decryptedKey = jceKeyCipher.decryptKey( + encryptedDataKey, keyName, decryptionMaterials.getEncryptionContext()); + return decryptionMaterials.withCleartextDataKey( + new SecretKeySpec(decryptedKey, decryptionMaterials.getAlgorithm().getDataKeyAlgo()), + new KeyringTraceEntry(keyNamespace, keyName, decryptTraceFlags())); + } catch (Exception e) { + LOGGER.info("Could not decrypt key due to: " + e.getMessage()); + } + } + } + + return decryptionMaterials; + } + + private EncryptionMaterials generateDataKey(EncryptionMaterials encryptionMaterials) { + final byte[] rawKey = new byte[encryptionMaterials.getAlgorithm().getDataKeyLength()]; + Utils.getSecureRandom().nextBytes(rawKey); + final SecretKey key = new SecretKeySpec(rawKey, encryptionMaterials.getAlgorithm().getDataKeyAlgo()); + + return encryptionMaterials.withCleartextDataKey(key, new KeyringTraceEntry(keyNamespace, keyName, GENERATED_DATA_KEY)); + } + + private KeyringTraceFlag[] encryptTraceFlags() { + if(jceKeyCipher.isEncryptionContextSigned()) { + return new KeyringTraceFlag[]{ENCRYPTED_DATA_KEY, SIGNED_ENCRYPTION_CONTEXT} ; + } else { + return new KeyringTraceFlag[]{ENCRYPTED_DATA_KEY}; + } + } + + private KeyringTraceFlag[] decryptTraceFlags() { + if(jceKeyCipher.isEncryptionContextSigned()) { + return new KeyringTraceFlag[]{DECRYPTED_DATA_KEY, VERIFIED_ENCRYPTION_CONTEXT} ; + } else { + return new KeyringTraceFlag[]{DECRYPTED_DATA_KEY}; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java new file mode 100644 index 000000000..9b8a7b453 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; + +/** + * A {@link Keyring} which does local RSA encryption and decryption of data keys using the + * provided public and private keys. + *

    + * Instantiate by using the {@code StandardKeyrings.rawRsaBuilder(...)} factory method. + */ +class RawRsaKeyring extends RawKeyring { + + RawRsaKeyring(String keyNamespace, String keyName, PublicKey publicKey, PrivateKey privateKey, RsaPaddingScheme rsaPaddingScheme) { + super(keyNamespace, keyName, JceKeyCipher.rsa(publicKey, privateKey, rsaPaddingScheme.getTransformation())); + } + + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + + // the key provider ID of the encrypted data key must + // have a value equal to this keyring's key namespace. + if (!keyNamespace.equals(encryptedDataKey.getProviderId())) { + return false; + } + + // the encrypted data key's key provider information + // must have a value equal to this keyring's key name. + if (!Arrays.equals(encryptedDataKey.getProviderInformation(), keyNameBytes)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringBuilder.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringBuilder.java new file mode 100644 index 000000000..f3e2a9aed --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringBuilder.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public class RawRsaKeyringBuilder { + private String keyNamespace; + private String keyName; + private PublicKey publicKey; + private PrivateKey privateKey; + private RsaPaddingScheme paddingScheme; + + private RawRsaKeyringBuilder() { + // Use RawRsaKeyringBuilder.standard() or StandardKeyrings.rawRsa() to instantiate + } + + /** + * Constructs a new instance of {@code RawRsaKeyringBuilder} + * + * @return The {@code RawRsaKeyringBuilder} + */ + public static RawRsaKeyringBuilder standard() { + return new RawRsaKeyringBuilder(); + } + + /** + * A value that, together with the key name, identifies the wrapping key (required). + * + * @param keyNamespace The key namespace + * @return The RawAesKeyringBuilder, for method chaining + */ + public RawRsaKeyringBuilder keyNamespace(String keyNamespace) { + this.keyNamespace = keyNamespace; + return this; + } + + /** + * A value that, together with the key namespace, identifies the wrapping key (required). + * + * @param keyName The key name + * @return The RawAesKeyringBuilder, for method chaining + */ + public RawRsaKeyringBuilder keyName(String keyName) { + this.keyName = keyName; + return this; + } + + /** + * The RSA public key used by this keyring to encrypt data keys. Not required when used for decryption. + * + * @param publicKey The public key + * @return The RawRsaKeyringBuilder, for method chaining + */ + public RawRsaKeyringBuilder publicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + /** + * The RSA private key used by this keyring to decrypt data keys. Not required when used for encryption. + * + * @param privateKey The public key + * @return The RawRsaKeyringBuilder, for method chaining + */ + public RawRsaKeyringBuilder privateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + return this; + } + + /** + * The RSA padding scheme to use with this keyring (required). + * + * @param paddingScheme The RSA padding scheme + * @return The RawRsaKeyringBuilder, for method chaining + */ + public RawRsaKeyringBuilder paddingScheme(RsaPaddingScheme paddingScheme) { + this.paddingScheme = paddingScheme; + return this; + } + + /** + * Constructs the {@link Keyring} instance. + * + * @return The {@link Keyring} instance + */ + public Keyring build() { + return new RawRsaKeyring(keyNamespace, keyName, publicKey, privateKey, paddingScheme); + } + + public enum RsaPaddingScheme { + + PKCS1("RSA/ECB/PKCS1Padding"), + OAEP_SHA1_MGF1("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"), + OAEP_SHA256_MGF1("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"), + OAEP_SHA384_MGF1("RSA/ECB/OAEPWithSHA-384AndMGF1Padding"), + OAEP_SHA512_MGF1("RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); + + private final String transformation; + + RsaPaddingScheme(String transformation) { + this.transformation = transformation; + } + + /** + * The Cipher transformation standard name as specified in + * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + * Note: In all cases the hash function used with MGF1 is the + * same as the hash function used directly with the message. + * + * @return The transformation name + */ + public String getTransformation() { + return transformation; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java new file mode 100644 index 000000000..1a3fc16f6 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java @@ -0,0 +1,125 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; + +import java.util.Arrays; +import java.util.List; + +/** + * Factory methods for instantiating the standard {@code Keyring}s provided by the AWS Encryption SDK. + */ +public class StandardKeyrings { + + private StandardKeyrings() { + } + + /** + * Returns a {@link RawAesKeyringBuilder} for use in constructing a keyring which does local AES-GCM encryption + * decryption of data keys using a provided wrapping key. + * + * @return The {@link RawAesKeyringBuilder} + */ + public static RawAesKeyringBuilder rawAesBuilder() { + return RawAesKeyringBuilder.standard(); + } + + /** + * Constructs a {@code RawRsaKeyringBuilder} which does local RSA encryption and decryption of data keys using the + * provided public and private keys. If {@code privateKey} is {@code null} then the returned {@code Keyring} + * can only be used for encryption. + * + * @return The {@link RawRsaKeyringBuilder} + */ + public static RawRsaKeyringBuilder rawRsaBuilder() { + return RawRsaKeyringBuilder.standard(); + } + + /** + * Constructs a {@code Keyring} which interacts with AWS Key Management Service (KMS) to create, + * encrypt, and decrypt data keys using the supplied AWS KMS defined Customer Master Key (CMK). + * Use {@link #awsKmsBuilder()} for more advanced configuration using an {@link AwsKmsKeyringBuilder} + * + * @param generatorKeyId An {@link AwsKmsCmkId} in ARN, CMK Alias, ARN Alias or Key Id format that identifies a + * AWS KMS CMK responsible for generating a data key, as well as encrypting and + * decrypting data keys . + * @return The {@code Keyring} + */ + public static Keyring awsKms(AwsKmsCmkId generatorKeyId) { + return AwsKmsKeyringBuilder.standard() + .generatorKeyId(generatorKeyId) + .build(); + } + + /** + * Returns a {@link AwsKmsKeyringBuilder} for use in constructing a keyring which interacts with + * AWS Key Management Service (KMS) to create, encrypt, and decrypt data keys using AWS KMS defined + * Customer Master Keys (CMKs). + * + * @return The {@link AwsKmsKeyringBuilder} + */ + public static AwsKmsKeyringBuilder awsKmsBuilder() { + return AwsKmsKeyringBuilder.standard(); + } + + /** + * Returns an {@link AwsKmsKeyringBuilder} for use in constructing an AWS KMS Discovery keyring. + * AWS KMS Discovery keyrings do not specify any CMKs to decrypt with, and thus will attempt to decrypt + * using any encrypted data key in an encrypted message. AWS KMS Discovery keyrings do not perform encryption. + *

    + * To create an AWS KMS Regional Discovery Keyring, use {@link StandardAwsKmsClientSuppliers#allowRegionsBuilder} or + * {@link StandardAwsKmsClientSuppliers#denyRegionsBuilder} to specify which regions to include/exclude. + *

    + * For example, to include only CMKs in the us-east-1 region: + *
    +     * StandardKeyrings.awsKmsDiscovery()
    +     *             .awsKmsClientSupplier(
    +     *                     StandardAwsKmsClientSuppliers.allowRegionsBuilder(Collections.singleton("us-east-1")).build()
    +     *             .build();
    +     * 
    + * + * @return The {@code AwsKmsKeyringBuilder} + */ + public static AwsKmsKeyringBuilder awsKmsDiscoveryBuilder() { + return AwsKmsKeyringBuilder.discovery(); + } + + /** + * Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call + * to modify the encryption or decryption materials using more than one keyring. + * + * @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty. + * @param childrenKeyrings A list of keyrings to be used to modify the encryption or decryption materials. + * At least one is required if generatorKeyring is null. + * @return The {@link Keyring} + */ + public static Keyring multi(Keyring generatorKeyring, List childrenKeyrings) { + return new MultiKeyring(generatorKeyring, childrenKeyrings); + } + + /** + * Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call + * to modify the encryption or decryption materials using more than one keyring. + * + * @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty. + * @param childrenKeyrings Keyrings to be used to modify the encryption or decryption materials. + * At least one is required if generatorKeyring is null. + * @return The {@link Keyring} + */ + public static Keyring multi(Keyring generatorKeyring, Keyring... childrenKeyrings) { + return new MultiKeyring(generatorKeyring, Arrays.asList(childrenKeyrings)); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsClientSupplier.java b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsClientSupplier.java new file mode 100644 index 000000000..92a0bfee1 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsClientSupplier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.arn.Arn; +import com.amazonaws.encryptionsdk.exception.UnsupportedRegionException; +import com.amazonaws.services.kms.AWSKMS; + +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a function that accepts an AWS region and returns an {@code AWSKMS} client for that region. The + * function should be able to handle when the region is null. + */ +@FunctionalInterface +public interface AwsKmsClientSupplier { + + /** + * Gets an {@code AWSKMS} client for the given regionId. + * + * @param regionId The AWS region (or null) + * @return The AWSKMS client + * @throws UnsupportedRegionException if a regionId is specified that this + * client supplier is configured to not allow. + */ + AWSKMS getClient(@Nullable String regionId) throws UnsupportedRegionException; + + /** + * Parses region from the given key id (if possible) and passes that region to the + * given clientSupplier to produce an {@code AWSKMS} client. + * + * @param keyId The Amazon Resource Name, Key Alias, Alias ARN or KeyId + * @param clientSupplier The client supplier + * @return AWSKMS The client + */ + static AWSKMS getClientByKeyId(AwsKmsCmkId keyId, AwsKmsClientSupplier clientSupplier) { + requireNonNull(keyId, "keyId is required"); + requireNonNull(clientSupplier, "clientSupplier is required"); + + if(keyId.isArn()) { + return clientSupplier.getClient(Arn.fromString(keyId.toString()).getRegion()); + } + + return clientSupplier.getClient(null); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkId.java b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkId.java new file mode 100644 index 000000000..200308514 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkId.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.arn.Arn; +import com.amazonaws.encryptionsdk.exception.MalformedArnException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import java.util.Objects; + +/** + * A representation of an AWS KMS Customer Master Key Identifier, which may be one either a + * key ID, key Amazon Resource Name (ARN), alias name, or alias ARN. + */ +public final class AwsKmsCmkId { + + private static final String ARN_PREFIX = "arn:"; + private String keyId; + + private AwsKmsCmkId(String keyId) throws MalformedArnException { + Validate.notBlank(keyId, "keyId must be neither null, empty nor whitespace"); + + if (keyId.startsWith(ARN_PREFIX)) { + try { + Arn.fromString(keyId); + } catch (IllegalArgumentException e) { + throw new MalformedArnException(e); + } + } + + this.keyId = keyId; + } + + /** + *

    + * Constructs an {@code AwsKmsCmkId} from the given String id. + *

    + *

    + * Valid identifiers must be either a key ID, key Amazon Resource Name (ARN), alias name, or alias ARN. When using + * an alias name, prefix it with "alias/". To specify a CMK in a different AWS account, you must use the key ARN or + * alias ARN. When using decryption operations, you must use the key ARN. + *

    + *

    + * For example: + *

    + *
      + *
    • + *

      + * Key ID: 1234abcd-12ab-34cd-56ef-1234567890ab + *

      + *
    • + *
    • + *

      + * Key ARN: arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab + *

      + *
    • + *
    • + *

      + * Alias name: alias/ExampleAlias + *

      + *
    • + *
    • + *

      + * Alias ARN: arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias + *

      + *
    • + *
    + * + * @param keyId The key ID, key Amazon Resource Name (ARN), alias name, or alias ARN + * @return The {@code AwsKmsCmkId} + * @throws MalformedArnException if the given keyId is an ARN (starts with 'arn:') and cannot be parsed + */ + public static AwsKmsCmkId fromString(String keyId) throws MalformedArnException { + return new AwsKmsCmkId(keyId); + } + + /** + * Returns true if the given keyId is a well formed Amazon Resource Name or is a Key Alias or raw Key Id. + * + * @param keyId The key ID, key Amazon Resource Name (ARN), alias name, or alias ARN + * @return True if well formed, false otherwise + */ + public static boolean isKeyIdWellFormed(String keyId) { + if (StringUtils.isBlank(keyId)) { + return false; + } + + if (!keyId.startsWith(ARN_PREFIX)) { + return true; + } + + try { + Arn.fromString(keyId); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Returns true if this AwsKmsCmkId is in the Amazon Resource Name (ARN) format. + * + * @return True if in the ARN format, false otherwise + */ + public boolean isArn() { + return keyId.startsWith(ARN_PREFIX); + } + + @Override + public String toString() { + return keyId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AwsKmsCmkId that = (AwsKmsCmkId) o; + return keyId.equals(that.keyId); + } + + @Override + public int hashCode() { + return Objects.hash(keyId); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDao.java b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDao.java new file mode 100644 index 000000000..cefbf97f3 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDao.java @@ -0,0 +1,172 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.exception.MismatchedDataKeyException; +import com.amazonaws.encryptionsdk.exception.UnsupportedRegionException; +import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import com.amazonaws.services.kms.model.DecryptRequest; +import com.amazonaws.services.kms.model.EncryptRequest; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static com.amazonaws.encryptionsdk.internal.Constants.AWS_KMS_PROVIDER_ID; +import static com.amazonaws.encryptionsdk.kms.AwsKmsClientSupplier.getClientByKeyId; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +/** + * An implementation of DataKeyEncryptionDao that uses AWS Key Management Service (KMS) for + * generation, encryption, and decryption of data keys. The KmsMethods interface is implemented + * to allow usage in KmsMasterKey. + */ +class AwsKmsDataKeyEncryptionDao implements DataKeyEncryptionDao, KmsMethods { + + private final AwsKmsClientSupplier clientSupplier; + private List grantTokens; + + AwsKmsDataKeyEncryptionDao(AwsKmsClientSupplier clientSupplier, List grantTokens) { + requireNonNull(clientSupplier, "clientSupplier is required"); + + this.clientSupplier = clientSupplier; + this.grantTokens = grantTokens == null ? new ArrayList<>() : new ArrayList<>(grantTokens); + } + + @Override + public GenerateDataKeyResult generateDataKey(AwsKmsCmkId keyId, CryptoAlgorithm algorithmSuite, Map encryptionContext) { + requireNonNull(keyId, "keyId is required"); + requireNonNull(algorithmSuite, "algorithmSuite is required"); + requireNonNull(encryptionContext, "encryptionContext is required"); + + final com.amazonaws.services.kms.model.GenerateDataKeyResult kmsResult; + + try { + kmsResult = getClientByKeyId(keyId, clientSupplier) + .generateDataKey(updateUserAgent( + new GenerateDataKeyRequest() + .withKeyId(keyId.toString()) + .withNumberOfBytes(algorithmSuite.getDataKeyLength()) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens))); + } catch (final AmazonServiceException ex) { + throw new AwsCryptoException(ex); + } + + final byte[] rawKey = new byte[algorithmSuite.getDataKeyLength()]; + kmsResult.getPlaintext().get(rawKey); + if (kmsResult.getPlaintext().remaining() > 0) { + throw new IllegalStateException("Received an unexpected number of bytes from KMS"); + } + final byte[] encryptedKey = new byte[kmsResult.getCiphertextBlob().remaining()]; + kmsResult.getCiphertextBlob().get(encryptedKey); + + return new GenerateDataKeyResult(new SecretKeySpec(rawKey, algorithmSuite.getDataKeyAlgo()), + new KeyBlob(AWS_KMS_PROVIDER_ID, kmsResult.getKeyId().getBytes(PROVIDER_ENCODING), encryptedKey)); + } + + @Override + public EncryptedDataKey encryptDataKey(final AwsKmsCmkId keyId, SecretKey plaintextDataKey, Map encryptionContext) { + requireNonNull(keyId, "keyId is required"); + requireNonNull(plaintextDataKey, "plaintextDataKey is required"); + requireNonNull(encryptionContext, "encryptionContext is required"); + isTrue(plaintextDataKey.getFormat().equals("RAW"), "Only RAW encoded keys are supported"); + + final com.amazonaws.services.kms.model.EncryptResult kmsResult; + + try { + kmsResult = getClientByKeyId(keyId, clientSupplier) + .encrypt(updateUserAgent(new EncryptRequest() + .withKeyId(keyId.toString()) + .withPlaintext(ByteBuffer.wrap(plaintextDataKey.getEncoded())) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens))); + } catch (final AmazonServiceException ex) { + throw new AwsCryptoException(ex); + } + final byte[] encryptedDataKey = new byte[kmsResult.getCiphertextBlob().remaining()]; + kmsResult.getCiphertextBlob().get(encryptedDataKey); + + return new KeyBlob(AWS_KMS_PROVIDER_ID, kmsResult.getKeyId().getBytes(PROVIDER_ENCODING), encryptedDataKey); + + } + + @Override + public DecryptDataKeyResult decryptDataKey(EncryptedDataKey encryptedDataKey, CryptoAlgorithm algorithmSuite, Map encryptionContext) { + requireNonNull(encryptedDataKey, "encryptedDataKey is required"); + requireNonNull(algorithmSuite, "algorithmSuite is required"); + requireNonNull(encryptionContext, "encryptionContext is required"); + + final String providerInformation = new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING); + final com.amazonaws.services.kms.model.DecryptResult kmsResult; + + try { + kmsResult = getClientByKeyId(AwsKmsCmkId.fromString(providerInformation), clientSupplier) + .decrypt(updateUserAgent(new DecryptRequest() + .withCiphertextBlob(ByteBuffer.wrap(encryptedDataKey.getEncryptedDataKey())) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens))); + } catch (final AmazonServiceException | UnsupportedRegionException ex) { + throw new CannotUnwrapDataKeyException(ex); + } + + if (!kmsResult.getKeyId().equals(providerInformation)) { + throw new MismatchedDataKeyException("Received an unexpected key Id from KMS"); + } + + final byte[] rawKey = new byte[algorithmSuite.getDataKeyLength()]; + kmsResult.getPlaintext().get(rawKey); + if (kmsResult.getPlaintext().remaining() > 0) { + throw new IllegalStateException("Received an unexpected number of bytes from KMS"); + } + + return new DecryptDataKeyResult(kmsResult.getKeyId(), new SecretKeySpec(rawKey, algorithmSuite.getDataKeyAlgo())); + + } + + private T updateUserAgent(T request) { + request.getRequestClientOptions().appendUserAgent(VersionInfo.USER_AGENT); + + return request; + } + + @Override + public void setGrantTokens(List grantTokens) { + this.grantTokens = new ArrayList<>(grantTokens); + } + + @Override + public List getGrantTokens() { + return Collections.unmodifiableList(grantTokens); + } + + @Override + public void addGrantToken(String grantToken) { + grantTokens.add(grantToken); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/DataKeyEncryptionDao.java b/src/main/java/com/amazonaws/encryptionsdk/kms/DataKeyEncryptionDao.java new file mode 100644 index 000000000..a68227b2b --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/DataKeyEncryptionDao.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; + +import javax.crypto.SecretKey; +import java.util.List; +import java.util.Map; + +public interface DataKeyEncryptionDao { + + /** + * Generates a unique data key, returning both the plaintext copy of the key and an encrypted copy encrypted using + * the customer master key specified by the given keyId. + * + * @param keyId The customer master key to encrypt the generated key with. + * @param algorithmSuite The algorithm suite associated with the key. + * @param encryptionContext The encryption context. + * @return GenerateDataKeyResult containing the plaintext data key and the encrypted data key. + */ + GenerateDataKeyResult generateDataKey(AwsKmsCmkId keyId, CryptoAlgorithm algorithmSuite, Map encryptionContext); + + /** + * Encrypts the given plaintext data key using the customer aster key specified by the given keyId. + * + * @param keyId The customer master key to encrypt the plaintext data key with. + * @param plaintextDataKey The plaintext data key to encrypt. + * @param encryptionContext The encryption context. + * @return The encrypted data key. + */ + EncryptedDataKey encryptDataKey(final AwsKmsCmkId keyId, SecretKey plaintextDataKey, Map encryptionContext); + + /** + * Decrypted the given encrypted data key. + * + * @param encryptedDataKey The encrypted data key to decrypt. + * @param algorithmSuite The algorithm suite associated with the key. + * @param encryptionContext The encryption context. + * @return DecryptDataKeyResult containing the plaintext data key and the ARN of the key that decrypted it. + */ + DecryptDataKeyResult decryptDataKey(EncryptedDataKey encryptedDataKey, CryptoAlgorithm algorithmSuite, Map encryptionContext); + + /** + * Constructs an instance of DataKeyEncryptionDao that uses AWS Key Management Service (KMS) for + * generation, encryption, and decryption of data keys. + * + * @param clientSupplier A supplier of AWSKMS clients + * @param grantTokens A list of grant tokens to supply to KMS + * @return The DataKeyEncryptionDao + */ + static DataKeyEncryptionDao awsKms(AwsKmsClientSupplier clientSupplier, List grantTokens) { + return new AwsKmsDataKeyEncryptionDao(clientSupplier, grantTokens); + } + + class GenerateDataKeyResult { + private final SecretKey plaintextDataKey; + private final EncryptedDataKey encryptedDataKey; + + public GenerateDataKeyResult(SecretKey plaintextDataKey, EncryptedDataKey encryptedDataKey) { + this.plaintextDataKey = plaintextDataKey; + this.encryptedDataKey = encryptedDataKey; + } + + public SecretKey getPlaintextDataKey() { + return plaintextDataKey; + } + + public EncryptedDataKey getEncryptedDataKey() { + return encryptedDataKey; + } + } + + class DecryptDataKeyResult { + private final String keyArn; + private final SecretKey plaintextDataKey; + + public DecryptDataKeyResult(String keyArn, SecretKey plaintextDataKey) { + this.keyArn = keyArn; + this.plaintextDataKey = plaintextDataKey; + } + + public String getKeyArn() { + return keyArn; + } + + public SecretKey getPlaintextDataKey() { + return plaintextDataKey; + } + + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java index b78840221..7cea5cd58 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java @@ -14,17 +14,12 @@ package com.amazonaws.encryptionsdk.kms; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.encryptionsdk.AwsCrypto; @@ -35,30 +30,22 @@ import com.amazonaws.encryptionsdk.MasterKeyProvider; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; -import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; import com.amazonaws.services.kms.AWSKMS; -import com.amazonaws.services.kms.model.DecryptRequest; -import com.amazonaws.services.kms.model.DecryptResult; -import com.amazonaws.services.kms.model.EncryptRequest; -import com.amazonaws.services.kms.model.EncryptResult; -import com.amazonaws.services.kms.model.GenerateDataKeyRequest; -import com.amazonaws.services.kms.model.GenerateDataKeyResult; + +import static java.util.Collections.emptyList; /** * Represents a single Customer Master Key (CMK) and is used to encrypt/decrypt data with * {@link AwsCrypto}. + * + * @deprecated Replaced by {@code KmsKeyring}. See {@link StandardKeyrings}. */ +@Deprecated public final class KmsMasterKey extends MasterKey implements KmsMethods { - private final Supplier kms_; + private final AwsKmsDataKeyEncryptionDao dataKeyEncryptionDao_; private final MasterKeyProvider sourceProvider_; private final String id_; - private final List grantTokens_ = new ArrayList<>(); - - private T updateUserAgent(T request) { - request.getRequestClientOptions().appendUserAgent(VersionInfo.USER_AGENT); - - return request; - } /** * @@ -80,11 +67,11 @@ public static KmsMasterKey getInstance(final AWSCredentialsProvider creds, final static KmsMasterKey getInstance(final Supplier kms, final String id, final MasterKeyProvider provider) { - return new KmsMasterKey(kms, id, provider); + return new KmsMasterKey(new AwsKmsDataKeyEncryptionDao(s -> kms.get(), emptyList()), id, provider); } - private KmsMasterKey(final Supplier kms, final String id, final MasterKeyProvider provider) { - kms_ = kms; + KmsMasterKey(final AwsKmsDataKeyEncryptionDao dataKeyEncryptionDao, final String id, final MasterKeyProvider provider) { + dataKeyEncryptionDao_ = dataKeyEncryptionDao; id_ = id; sourceProvider_ = provider; } @@ -102,39 +89,27 @@ public String getKeyId() { @Override public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { - final GenerateDataKeyResult gdkResult = kms_.get().generateDataKey(updateUserAgent( - new GenerateDataKeyRequest() - .withKeyId(getKeyId()) - .withNumberOfBytes(algorithm.getDataKeyLength()) - .withEncryptionContext(encryptionContext) - .withGrantTokens(grantTokens_) - )); - final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; - gdkResult.getPlaintext().get(rawKey); - if (gdkResult.getPlaintext().remaining() > 0) { - throw new IllegalStateException("Recieved an unexpected number of bytes from KMS"); - } - final byte[] encryptedKey = new byte[gdkResult.getCiphertextBlob().remaining()]; - gdkResult.getCiphertextBlob().get(encryptedKey); - - final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); - return new DataKey<>(key, encryptedKey, gdkResult.getKeyId().getBytes(StandardCharsets.UTF_8), this); + final DataKeyEncryptionDao.GenerateDataKeyResult gdkResult = dataKeyEncryptionDao_.generateDataKey( + AwsKmsCmkId.fromString(getKeyId()), algorithm, encryptionContext); + return new DataKey<>(gdkResult.getPlaintextDataKey(), + gdkResult.getEncryptedDataKey().getEncryptedDataKey(), + gdkResult.getEncryptedDataKey().getProviderInformation(), + this); } @Override public void setGrantTokens(final List grantTokens) { - grantTokens_.clear(); - grantTokens_.addAll(grantTokens); + dataKeyEncryptionDao_.setGrantTokens(grantTokens); } @Override public List getGrantTokens() { - return grantTokens_; + return dataKeyEncryptionDao_.getGrantTokens(); } @Override public void addGrantToken(final String grantToken) { - grantTokens_.add(grantToken); + dataKeyEncryptionDao_.addGrantToken(grantToken); } @Override @@ -142,22 +117,13 @@ public DataKey encryptDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext, final DataKey dataKey) { final SecretKey key = dataKey.getKey(); - if (!key.getFormat().equals("RAW")) { - throw new IllegalArgumentException("Only RAW encoded keys are supported"); - } - try { - final EncryptResult encryptResult = kms_.get().encrypt(updateUserAgent( - new EncryptRequest() - .withKeyId(id_) - .withPlaintext(ByteBuffer.wrap(key.getEncoded())) - .withEncryptionContext(encryptionContext) - .withGrantTokens(grantTokens_))); - final byte[] edk = new byte[encryptResult.getCiphertextBlob().remaining()]; - encryptResult.getCiphertextBlob().get(edk); - return new DataKey<>(dataKey.getKey(), edk, encryptResult.getKeyId().getBytes(StandardCharsets.UTF_8), this); - } catch (final AmazonServiceException asex) { - throw new AwsCryptoException(asex); - } + final EncryptedDataKey encryptedDataKey = dataKeyEncryptionDao_.encryptDataKey( + AwsKmsCmkId.fromString(id_), key, encryptionContext); + + return new DataKey<>(dataKey.getKey(), + encryptedDataKey.getEncryptedDataKey(), + encryptedDataKey.getProviderInformation(), + this); } @Override @@ -168,24 +134,13 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, final List exceptions = new ArrayList<>(); for (final EncryptedDataKey edk : encryptedDataKeys) { try { - final DecryptResult decryptResult = kms_.get().decrypt(updateUserAgent( - new DecryptRequest() - .withCiphertextBlob(ByteBuffer.wrap(edk.getEncryptedDataKey())) - .withEncryptionContext(encryptionContext) - .withGrantTokens(grantTokens_))); - if (decryptResult.getKeyId().equals(id_)) { - final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; - decryptResult.getPlaintext().get(rawKey); - if (decryptResult.getPlaintext().remaining() > 0) { - throw new IllegalStateException("Received an unexpected number of bytes from KMS"); - } - return new DataKey<>( - new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), - edk.getEncryptedDataKey(), - edk.getProviderInformation(), this); - } - } catch (final AmazonServiceException awsex) { - exceptions.add(awsex); + final DataKeyEncryptionDao.DecryptDataKeyResult result = dataKeyEncryptionDao_.decryptDataKey(edk, algorithm, encryptionContext); + return new DataKey<>( + result.getPlaintextDataKey(), + edk.getEncryptedDataKey(), + edk.getProviderInformation(), this); + } catch (final AwsCryptoException ex) { + exceptions.add(ex); } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java index 9f0d91b42..36e48a635 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java @@ -43,6 +43,7 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; import com.amazonaws.handlers.RequestHandler2; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; @@ -54,7 +55,10 @@ /** * Provides {@link MasterKey}s backed by the AWS Key Management Service. This object is regional and * if you want to use keys from multiple regions, you'll need multiple copies of this object. + * + * @deprecated Replaced by {@code KmsKeyring}. See {@link StandardKeyrings}. */ +@Deprecated public class KmsMasterKeyProvider extends MasterKeyProvider implements KmsMethods { private static final String PROVIDER_NAME = "aws-kms"; private final List keyIds_; diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliers.java b/src/main/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliers.java new file mode 100644 index 000000000..24b71c45c --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliers.java @@ -0,0 +1,257 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.encryptionsdk.exception.UnsupportedRegionException; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClientBuilder; +import com.amazonaws.services.kms.model.AWSKMSException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.notEmpty; + +/** + * Factory methods for instantiating the standard {@code AwsKmsClientSupplier}s provided by the AWS Encryption SDK. + */ +public class StandardAwsKmsClientSuppliers { + + /** + * A builder to construct the default AwsKmsClientSupplier that will create and cache clients + * for any region. Credentials and client configuration may be specified if necessary. + * + * @return The builder + */ + public static DefaultAwsKmsClientSupplierBuilder defaultBuilder() { + return new DefaultAwsKmsClientSupplierBuilder(AWSKMSClientBuilder.standard()); + } + + /** + * A builder to construct an AwsKmsClientSupplier that will + * only supply clients for a given set of AWS regions. + * + * @param allowedRegions the AWS regions that the client supplier is allowed to supply clients for + * @return The builder + */ + public static AllowRegionsAwsKmsClientSupplierBuilder allowRegionsBuilder(Set allowedRegions) { + return new AllowRegionsAwsKmsClientSupplierBuilder(allowedRegions); + } + + /** + * A builder to construct an AwsKmsClientSupplier that will + * supply clients for all AWS regions except the given set of regions. + * + * @param deniedRegions the AWS regions that the client supplier will not supply clients for + * @return The builder + */ + public static DenyRegionsAwsKmsClientSupplierBuilder denyRegionsBuilder(Set deniedRegions) { + return new DenyRegionsAwsKmsClientSupplierBuilder(deniedRegions); + } + + + /** + * Builder to construct an AwsKmsClientSupplier that will create and cache clients + * for any region. CredentialProvider and ClientConfiguration are optional and may + * be configured if necessary. + */ + public static class DefaultAwsKmsClientSupplierBuilder { + + private AWSCredentialsProvider credentialsProvider; + private ClientConfiguration clientConfiguration; + private final Map clientsCache = new ConcurrentHashMap<>(); + private static final Set AWSKMS_METHODS = new HashSet<>(); + private AWSKMSClientBuilder awsKmsClientBuilder; + private static final String NULL_REGION = "null-region"; + + static { + AWSKMS_METHODS.add("generateDataKey"); + AWSKMS_METHODS.add("encrypt"); + AWSKMS_METHODS.add("decrypt"); + } + + DefaultAwsKmsClientSupplierBuilder(AWSKMSClientBuilder awsKmsClientBuilder) { + this.awsKmsClientBuilder = awsKmsClientBuilder; + } + + public AwsKmsClientSupplier build() { + + return regionId -> { + + if(regionId == null) { + regionId = NULL_REGION; + } + + if (clientsCache.containsKey(regionId)) { + return clientsCache.get(regionId); + } + + if (credentialsProvider != null) { + awsKmsClientBuilder = awsKmsClientBuilder.withCredentials(credentialsProvider); + } + + if (clientConfiguration != null) { + awsKmsClientBuilder = awsKmsClientBuilder.withClientConfiguration(clientConfiguration); + } + + if (!regionId.equals(NULL_REGION)) { + awsKmsClientBuilder = awsKmsClientBuilder.withRegion(regionId); + } + + return newCachingProxy(awsKmsClientBuilder.build(), regionId); + }; + } + + /** + * Sets the AWSCredentialsProvider used by the client. + * + * @param credentialsProvider New AWSCredentialsProvider to use. + */ + public DefaultAwsKmsClientSupplierBuilder credentialsProvider(AWSCredentialsProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + /** + * Sets the ClientConfiguration to be used by the client. + * + * @param clientConfiguration Custom configuration to use. + */ + public DefaultAwsKmsClientSupplierBuilder clientConfiguration(ClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + return this; + } + + /** + * Creates a proxy for the AWSKMS client that will populate the client into the client cache + * after an AWS KMS method successfully completes or an AWS KMS exception occurs. This is to prevent a + * a malicious user from causing a local resource DOS by sending ciphertext with a large number + * of spurious regions, thereby filling the cache with regions and exhausting resources. + * + * @param client The client to proxy + * @param regionId The region the client is associated with + * @return The proxy + */ + private AWSKMS newCachingProxy(AWSKMS client, String regionId) { + return (AWSKMS) Proxy.newProxyInstance( + AWSKMS.class.getClassLoader(), + new Class[]{AWSKMS.class}, + (proxy, method, methodArgs) -> { + try { + final Object result = method.invoke(client, methodArgs); + if (AWSKMS_METHODS.contains(method.getName())) { + clientsCache.put(regionId, client); + } + return result; + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof AWSKMSException && + AWSKMS_METHODS.contains(method.getName())) { + clientsCache.put(regionId, client); + } + + throw e.getTargetException(); + } + }); + } + } + + /** + * An AwsKmsClientSupplier that will only supply clients for a given set of AWS regions. + */ + public static class AllowRegionsAwsKmsClientSupplierBuilder { + + private final Set allowedRegions; + private AwsKmsClientSupplier baseClientSupplier = StandardAwsKmsClientSuppliers.defaultBuilder().build(); + + private AllowRegionsAwsKmsClientSupplierBuilder(Set allowedRegions) { + notEmpty(allowedRegions, "At least one region is required"); + requireNonNull(baseClientSupplier, "baseClientSupplier is required"); + + this.allowedRegions = allowedRegions; + } + + /** + * Constructs the AwsKmsClientSupplier. + * + * @return The AwsKmsClientSupplier + */ + public AwsKmsClientSupplier build() { + return regionId -> { + + if (!allowedRegions.contains(regionId)) { + throw new UnsupportedRegionException(String.format("Region %s is not in the set of allowed regions %s", + regionId, allowedRegions)); + } + + return baseClientSupplier.getClient(regionId); + }; + } + + /** + * Sets the client supplier that will supply the client if the region is allowed. + * + * @param baseClientSupplier the client supplier that will supply the client if the region is allowed. + */ + public AllowRegionsAwsKmsClientSupplierBuilder baseClientSupplier(AwsKmsClientSupplier baseClientSupplier) { + this.baseClientSupplier = baseClientSupplier; + return this; + } + } + + /** + * A client supplier that supplies clients for any region except the specified AWS regions. + */ + public static class DenyRegionsAwsKmsClientSupplierBuilder { + + private final Set deniedRegions; + private AwsKmsClientSupplier baseClientSupplier = StandardAwsKmsClientSuppliers.defaultBuilder().build(); + + private DenyRegionsAwsKmsClientSupplierBuilder(Set deniedRegions) { + notEmpty(deniedRegions, "At least one region is required"); + requireNonNull(baseClientSupplier, "baseClientSupplier is required"); + + this.deniedRegions = deniedRegions; + } + + /** + * Sets the client supplier that will supply the client if the region is allowed. + * + * @param baseClientSupplier the client supplier that will supply the client if the region is allowed. + */ + public DenyRegionsAwsKmsClientSupplierBuilder baseClientSupplier(AwsKmsClientSupplier baseClientSupplier) { + this.baseClientSupplier = baseClientSupplier; + return this; + } + + public AwsKmsClientSupplier build() { + + return regionId -> { + + if (deniedRegions.contains(regionId)) { + throw new UnsupportedRegionException(String.format("Region %s is in the set of denied regions %s", + regionId, deniedRegions)); + } + + return baseClientSupplier.getClient(regionId); + }; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java index 94423b884..0ad194cc2 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java @@ -1,26 +1,101 @@ package com.amazonaws.encryptionsdk.model; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceEntry; + +import javax.crypto.SecretKey; import java.security.PublicKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; -import com.amazonaws.encryptionsdk.DataKey; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; +import static org.apache.commons.lang3.Validate.isTrue; public final class DecryptionMaterials { + private final CryptoAlgorithm algorithm; + private final Map encryptionContext; private final DataKey dataKey; private final PublicKey trailingSignatureKey; + private final KeyringTrace keyringTrace; private DecryptionMaterials(Builder b) { + algorithm = b.algorithm; + encryptionContext = b.encryptionContext; dataKey = b.getDataKey(); trailingSignatureKey = b.getTrailingSignatureKey(); + keyringTrace = b.keyringTrace; } + /** + * The algorithm suite to use for this decryption operation. + */ + public CryptoAlgorithm getAlgorithm() { + return algorithm; + } + + /** + * The encryption context + */ + public Map getEncryptionContext() { + return encryptionContext; + } + + /** + * @deprecated Replaced by {@link #getCleartextDataKey()} + */ + @Deprecated public DataKey getDataKey() { return dataKey; } + /** + * Creates a new {@code DecryptionMaterials} instance based on this instance with the addition of the + * provided cleartext data key and keyring trace entry. The cleartext data key must not already be populated. + * + * @param cleartextDataKey The cleartext data key. + * @param keyringTraceEntry The keyring trace entry recording this action. + * @return The new {@code DecryptionMaterials} instance. + */ + public DecryptionMaterials withCleartextDataKey(SecretKey cleartextDataKey, KeyringTraceEntry keyringTraceEntry) { + if (hasCleartextDataKey()) { + throw new IllegalStateException("cleartextDataKey was already populated"); + } + requireNonNull(cleartextDataKey, "cleartextDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + validateCleartextDataKey(algorithm, cleartextDataKey); + + return toBuilder() + .setCleartextDataKey(cleartextDataKey) + .setKeyringTrace(keyringTrace.with(keyringTraceEntry)) + .build(); + } + + public SecretKey getCleartextDataKey() { + return dataKey == null ? null : dataKey.getKey(); + } + + /** + * Returns true if a cleartext data key has been populated. + * + * @return True if cleartext data key is populated, false otherwise. + */ + public boolean hasCleartextDataKey() { + return this.dataKey != null; + } + public PublicKey getTrailingSignatureKey() { return trailingSignatureKey; } + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + public static Builder newBuilder() { return new Builder(); } @@ -29,26 +104,94 @@ public Builder toBuilder() { return new Builder(this); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DecryptionMaterials that = (DecryptionMaterials) o; + + return algorithm == that.algorithm && + Objects.equals(getCleartextDataKey(), that.getCleartextDataKey()) && + Objects.equals(trailingSignatureKey, that.trailingSignatureKey) && + Objects.equals(encryptionContext, that.encryptionContext) && + Objects.equals(keyringTrace, that.keyringTrace); + } + + @Override + public int hashCode() { + return Objects.hash(algorithm, getCleartextDataKey(), trailingSignatureKey, encryptionContext, keyringTrace); + } + + private void validateCleartextDataKey(CryptoAlgorithm algorithm, SecretKey cleartextDataKey) throws IllegalArgumentException { + if (algorithm != null && cleartextDataKey != null) { + isTrue(algorithm.getDataKeyLength() == cleartextDataKey.getEncoded().length, + String.format("Incorrect key length. Expected %s but got %s", + algorithm.getDataKeyLength(), cleartextDataKey.getEncoded().length)); + isTrue(algorithm.getDataKeyAlgo().equalsIgnoreCase(cleartextDataKey.getAlgorithm()), + String.format("Incorrect key algorithm. Expected %s but got %s", + algorithm.getDataKeyAlgo(), cleartextDataKey.getAlgorithm())); + } + } + public static final class Builder { + private CryptoAlgorithm algorithm; + private Map encryptionContext = Collections.emptyMap(); private DataKey dataKey; private PublicKey trailingSignatureKey; + private KeyringTrace keyringTrace = KeyringTrace.EMPTY_TRACE; private Builder(DecryptionMaterials result) { + this.algorithm = result.getAlgorithm(); + this.encryptionContext = result.getEncryptionContext(); this.dataKey = result.getDataKey(); this.trailingSignatureKey = result.getTrailingSignatureKey(); + this.keyringTrace = result.getKeyringTrace(); } private Builder() {} + public CryptoAlgorithm getAlgorithm() { + return algorithm; + } + + public Builder setAlgorithm(CryptoAlgorithm algorithm) { + requireNonNull(algorithm, "algorithm is required"); + this.algorithm = algorithm; + return this; + } + + public Map getEncryptionContext() { + return encryptionContext; + } + + public Builder setEncryptionContext(Map encryptionContext) { + requireNonNull(encryptionContext, "encryptionContext is required"); + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + @Deprecated public DataKey getDataKey() { return dataKey; } + @Deprecated public Builder setDataKey(DataKey dataKey) { this.dataKey = dataKey; return this; } + /** + * Sets the cleartext data key. + * + * @param cleartextDataKey The cleartext data key. + */ + public Builder setCleartextDataKey(SecretKey cleartextDataKey) { + requireNonNull(cleartextDataKey, "cleartextDataKey is required"); + this.dataKey = new DataKey<>(cleartextDataKey, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY, null); + return this; + } + public PublicKey getTrailingSignatureKey() { return trailingSignatureKey; } @@ -58,6 +201,16 @@ public Builder setTrailingSignatureKey(PublicKey trailingSignatureKey) { return this; } + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + public Builder setKeyringTrace(KeyringTrace keyringTrace) { + requireNonNull(keyringTrace, "keyringTrace is required"); + this.keyringTrace = keyringTrace; + return this; + } + public DecryptionMaterials build() { return new DecryptionMaterials(this); } diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java index 1a40d7c36..215ed4012 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java @@ -1,5 +1,11 @@ package com.amazonaws.encryptionsdk.model; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceEntry; + import javax.crypto.SecretKey; import java.security.PrivateKey; import java.util.ArrayList; @@ -9,8 +15,10 @@ import java.util.Map; import java.util.Objects; -import com.amazonaws.encryptionsdk.CryptoAlgorithm; -import com.amazonaws.encryptionsdk.MasterKey; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; /** * Contains the cryptographic materials needed for an encryption operation. @@ -24,6 +32,7 @@ public final class EncryptionMaterials { private final SecretKey cleartextDataKey; private final PrivateKey trailingSignatureKey; private final List masterKeys; + private final KeyringTrace keyringTrace; private EncryptionMaterials(Builder b) { this.algorithm = b.algorithm; @@ -32,6 +41,7 @@ private EncryptionMaterials(Builder b) { this.cleartextDataKey = b.cleartextDataKey; this.trailingSignatureKey = b.trailingSignatureKey; this.masterKeys = b.getMasterKeys(); + this.keyringTrace = b.keyringTrace; } public Builder toBuilder() { @@ -64,6 +74,27 @@ public List getEncryptedDataKeys() { return encryptedDataKeys; } + /** + * Creates a new {@code EncryptionMaterials} instance based on this instance with the addition of the + * provided encrypted data key and keyring trace entry. + * + * @param encryptedDataKey The encrypted data key to add. + * @param keyringTraceEntry The keyring trace entry recording this action. + * @return The new {@code EncryptionMaterials} instance. + */ + public EncryptionMaterials withEncryptedDataKey(KeyBlob encryptedDataKey, KeyringTraceEntry keyringTraceEntry) { + requireNonNull(encryptedDataKey, "encryptedDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + + final List encryptedDataKeys = new ArrayList<>(getEncryptedDataKeys()); + encryptedDataKeys.add(encryptedDataKey); + + return toBuilder() + .setEncryptedDataKeys(encryptedDataKeys) + .setKeyringTrace(keyringTrace.with(keyringTraceEntry)) + .build(); + } + /** * The cleartext data key to use for encrypting this message. Note that this is the data key prior to * any key derivation required by the crypto algorithm in use. @@ -72,6 +103,37 @@ public SecretKey getCleartextDataKey() { return cleartextDataKey; } + /** + * Creates a new {@code EncryptionMaterials} instance based on this instance with the addition of the + * provided cleartext data key and keyring trace entry. The cleartext data key must not already be populated. + * + * @param cleartextDataKey The cleartext data key. + * @param keyringTraceEntry The keyring trace entry recording this action. + * @return The new {@code EncryptionMaterials} instance. + */ + public EncryptionMaterials withCleartextDataKey(SecretKey cleartextDataKey, KeyringTraceEntry keyringTraceEntry) { + if (hasCleartextDataKey()) { + throw new IllegalStateException("cleartextDataKey was already populated"); + } + requireNonNull(cleartextDataKey, "cleartextDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + validateCleartextDataKey(algorithm, cleartextDataKey); + + return toBuilder() + .setCleartextDataKey(cleartextDataKey) + .setKeyringTrace(keyringTrace.with(keyringTraceEntry)) + .build(); + } + + /** + * Returns true if a cleartext data key has been populated. + * + * @return True is a cleartext data key has been populated, false otherwise. + */ + public boolean hasCleartextDataKey() { + return this.cleartextDataKey != null; + } + /** * The private key to be used to sign the message trailer. Must be present if any only if required by the * crypto algorithm, and the key type must likewise match the algorithm in use. @@ -86,11 +148,36 @@ public PrivateKey getTrailingSignatureKey() { /** * Contains a list of all MasterKeys that could decrypt this message. + * + * @deprecated {@link MasterKey}s have been replaced by {@link Keyring}s */ + @Deprecated public List getMasterKeys() { return masterKeys; } + /** + * A keyring trace containing all of the actions that keyrings have taken on this set of encryption materials. + */ + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + /** + * Validates that the given plaintext data key fits the specification + * for the data key algorithm specified in the given algorithm suite. + */ + private void validateCleartextDataKey(CryptoAlgorithm algorithmSuite, SecretKey cleartextDataKey) throws IllegalArgumentException { + if (algorithmSuite != null && cleartextDataKey != null) { + isTrue(algorithmSuite.getDataKeyLength() == cleartextDataKey.getEncoded().length, + String.format("Incorrect data key length. Expected %s but got %s", + algorithmSuite.getDataKeyLength(), cleartextDataKey.getEncoded().length)); + isTrue(algorithmSuite.getDataKeyAlgo().equalsIgnoreCase(cleartextDataKey.getAlgorithm()), + String.format("Incorrect data key algorithm. Expected %s but got %s", + algorithmSuite.getDataKeyAlgo(), cleartextDataKey.getAlgorithm())); + } + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; @@ -100,21 +187,23 @@ public List getMasterKeys() { Objects.equals(encryptedDataKeys, that.encryptedDataKeys) && Objects.equals(cleartextDataKey, that.cleartextDataKey) && Objects.equals(trailingSignatureKey, that.trailingSignatureKey) && - Objects.equals(masterKeys, that.masterKeys); + Objects.equals(masterKeys, that.masterKeys) && + Objects.equals(keyringTrace, that.keyringTrace); } @Override public int hashCode() { return Objects.hash(algorithm, encryptionContext, encryptedDataKeys, cleartextDataKey, trailingSignatureKey, - masterKeys); + masterKeys, keyringTrace); } public static class Builder { private CryptoAlgorithm algorithm; private Map encryptionContext = Collections.emptyMap(); - private List encryptedDataKeys = null; + private List encryptedDataKeys = Collections.emptyList(); private SecretKey cleartextDataKey; private PrivateKey trailingSignatureKey; private List masterKeys = Collections.emptyList(); + private KeyringTrace keyringTrace = KeyringTrace.EMPTY_TRACE; private Builder() {} @@ -125,6 +214,7 @@ private Builder(EncryptionMaterials r) { cleartextDataKey = r.cleartextDataKey; trailingSignatureKey = r.trailingSignatureKey; setMasterKeys(r.masterKeys); + keyringTrace = r.keyringTrace; } public EncryptionMaterials build() { @@ -145,7 +235,7 @@ public Map getEncryptionContext() { } public Builder setEncryptionContext(Map encryptionContext) { - this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + this.encryptionContext = unmodifiableMap(new HashMap<>(encryptionContext)); return this; } @@ -154,7 +244,7 @@ public List getEncryptedDataKeys() { } public Builder setEncryptedDataKeys(List encryptedDataKeys) { - this.encryptedDataKeys = Collections.unmodifiableList(new ArrayList<>(encryptedDataKeys)); + this.encryptedDataKeys = unmodifiableList(new ArrayList<>(encryptedDataKeys)); return this; } @@ -176,12 +266,23 @@ public Builder setTrailingSignatureKey(PrivateKey trailingSignatureKey) { return this; } + @Deprecated public List getMasterKeys() { return masterKeys; } + @Deprecated public Builder setMasterKeys(List masterKeys) { - this.masterKeys = Collections.unmodifiableList(new ArrayList<>(masterKeys)); + this.masterKeys = unmodifiableList(new ArrayList<>(masterKeys)); + return this; + } + + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + public Builder setKeyringTrace(KeyringTrace keyringTrace) { + this.keyringTrace = keyringTrace; return this; } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java b/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java index c44fd2f8f..dbea9f6b4 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java @@ -542,7 +542,7 @@ public int getKeyProviderIdLen() { */ @Override public String getProviderId() { - String s = new String(keyProviderId_, StandardCharsets.UTF_8); + String s = new String(keyProviderId_, PROVIDER_ENCODING); // The following assume statement essentially says that different // calls to the String constructor above, with the same parameters, // result in strings with the same contents. The assumption is @@ -627,7 +627,7 @@ public byte[] getEncryptedDataKey() { //@ assignable \nothing; //@ signals_only AwsCryptoException; public void setKeyProviderId(final String keyProviderId) { - final byte[] keyProviderIdBytes = keyProviderId.getBytes(StandardCharsets.UTF_8); + final byte[] keyProviderIdBytes = keyProviderId.getBytes(PROVIDER_ENCODING); //@ assume Arrays.equalArrays(keyProviderIdBytes, EncryptedDataKey.s2ba(keyProviderId)); if (keyProviderIdBytes.length > Constants.UNSIGNED_SHORT_MAX_VAL) { throw new AwsCryptoException( diff --git a/src/main/java/com/amazonaws/encryptionsdk/multi/MultipleProviderFactory.java b/src/main/java/com/amazonaws/encryptionsdk/multi/MultipleProviderFactory.java index 00b723d47..b1a65f803 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/multi/MultipleProviderFactory.java +++ b/src/main/java/com/amazonaws/encryptionsdk/multi/MultipleProviderFactory.java @@ -29,6 +29,7 @@ import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; /** * Constructs {@link MasterKeyProvider}s which are backed by any number of other @@ -46,7 +47,10 @@ * * All methods in this factory return identical results and exist only for different degrees of * type-safety. + * + * @deprecated Replaced by {@code MultiKeyring}. See {@link StandardKeyrings}. */ +@Deprecated public class MultipleProviderFactory { private MultipleProviderFactory() { // Prevent instantiation diff --git a/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java deleted file mode 100644 index 5d162679d..000000000 --- a/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.amazonaws.crypto.examples; - -import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; -import org.junit.Test; - -public class BasicEncryptionExampleTest { - - @Test - public void testEncryptAndDecrypt() { - BasicEncryptionExample.encryptAndDecrypt(KMSTestFixtures.TEST_KEY_IDS[0]); - } -} diff --git a/src/test/java/com/amazonaws/crypto/examples/ExamplesTest.java b/src/test/java/com/amazonaws/crypto/examples/ExamplesTest.java new file mode 100644 index 000000000..26b9e3ee6 --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/ExamplesTest.java @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.TestUtils; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestFactory; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.junit.platform.commons.support.ReflectionSupport; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.fail; + +@Tag(TestUtils.TAG_INTEGRATION) +class ExamplesTest { + + private static final Logger LOGGER = Logger.getLogger(ExamplesTest.class.getName()); + private static final String RUN_METHOD_NAME = "run"; + private static final String TEST_CLASS_SUFFIX = "Test"; + private static final byte[] STATIC_PLAINTEXT = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Praesent non feugiat leo. Aenean iaculis tellus ut velit consectetur, " + + "quis convallis orci eleifend. Sed eu dictum sapien. Nulla facilisi. Suspendisse potenti. " + + "Proin vehicula vehicula maximus. Donec varius et elit vel rutrum. Nulla lacinia neque turpis," + + " quis consequat orci pharetra et. Etiam consequat ullamcorper mauris. Vivamus molestie mollis " + + "mauris a gravida. Curabitur sed bibendum nisl. Cras varius tortor non erat sodales, quis congue" + + " tellus laoreet. Etiam fermentum purus eu diam sagittis, vitae commodo est vehicula. " + + "Nulla feugiat viverra orci vel interdum. Quisque pulvinar elit eget nulla facilisis varius. " + + "Mauris at suscipit sem. Aliquam in purus ut velit fringilla volutpat id non mi. " + + "Curabitur quis nunc eleifend, ornare lectus non, fringilla quam. Nam maximus volutpat placerat. " + + "Nulla ullamcorper lorem velit, nec sagittis ex tristique posuere. Aliquam fringilla magna commodo" + + " libero faucibus tempor. Vestibulum non ligula tincidunt, finibus sapien in, sollicitudin " + + "ex. Pellentesque congue laoreet mi in condimentum. Cras convallis nisi ac nunc tincidunt " + + "venenatis. Suspendisse urna elit, cursus eu lacus a, aliquet porttitor mi. " + + "Nulla vel congue nibh, sed condimentum dui. Ut ante ligula, blandit eu finibus nec, " + + "scelerisque quis eros. Maecenas gravida odio eget nibh dictum, dictum varius lacus interdum. " + + "Integer quis nulla vulputate, rhoncus diam vitae, mollis mauris. Sed ut porttitor dolor. " + + "Fusce ut justo a ex bibendum imperdiet nec sit amet magna. Sed ullamcorper luctus augue, " + + "tempor viverra elit interdum sed. Cras sit amet arcu eu turpis molestie sollicitudin. " + + "Curabitur fermentum varius nibh, ut aliquet nisi. Aliquam id tempus tellus. " + + "Nulla porttitor nulla at nibh interdum, quis sollicitudin erat egestas. " + + "Ut blandit mauris quis efficitur efficitur. Morbi neque sapien, posuere ut aliquam eget, " + + "aliquam at velit. Morbi sit amet rhoncus felis, et hendrerit sem. Nulla porta dictum ligula " + + "eget iaculis. Cras lacinia ligula quis risus ultrices, sed consectetur metus imperdiet. " + + "Nullam id enim vestibulum nibh ultricies auctor. Morbi neque lacus, faucibus vitae commodo quis, " + + "malesuada sed velit.").getBytes(StandardCharsets.UTF_8); + + @TestFactory + Stream testExamples() { + final List> exampleClasses = ReflectionSupport.findAllClassesInPackage(getClass().getPackage().getName(), + c -> Arrays.stream(c.getDeclaredMethods()).anyMatch(m -> m.getName().equals(RUN_METHOD_NAME)), + c -> !c.endsWith(TEST_CLASS_SUFFIX)); + + return exampleClasses.stream() + .map(c -> ReflectionSupport.findMethods(c, m -> m.getName().equals(RUN_METHOD_NAME), HierarchyTraversalMode.TOP_DOWN).get(0)) + .map(ExamplesTest::createTest); + } + + /** + * Creates a DynamicTest for the given method, matching each parameter type + * to the 4 parameter types that we have predefined values for. + */ + private static DynamicTest createTest(Method method) { + final Class[] parameterTypes = method.getParameterTypes(); + final Object[] parameterValues = new Object[parameterTypes.length]; + + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i].isAssignableFrom(AwsKmsCmkId.class)) { + parameterValues[i] = AwsKmsCmkId.fromString(KMSTestFixtures.TEST_KEY_IDS[0]); + } else if (parameterTypes[i].isAssignableFrom(List.class)) { + parameterValues[i] = Collections.singletonList(AwsKmsCmkId.fromString(KMSTestFixtures.TEST_KEY_IDS[1])); + } else if (parameterTypes[i].isAssignableFrom(byte[].class)) { + parameterValues[i] = STATIC_PLAINTEXT; + } else if (parameterTypes[i].isAssignableFrom(File.class)) { + try { + final File tempFile = File.createTempFile(method.getDeclaringClass().getSimpleName(), ".tmp"); + tempFile.deleteOnExit(); + + try (OutputStream os = Files.newOutputStream(tempFile.toPath())) { + os.write(STATIC_PLAINTEXT); + } + + parameterValues[i] = tempFile; + } catch (IOException e) { + fail("Failed to create temp file", e); + } + } else { + LOGGER.info(String.format("Setting unsupported parameter type[%s] to null", parameterTypes[i])); + parameterValues[i] = null; + } + } + + return DynamicTest.dynamicTest(method.getDeclaringClass().getName(), () -> { + try { + method.invoke(null, parameterValues); + } catch (IllegalAccessException e) { + fail(method.getDeclaringClass().getName() + " failed", e); + } catch (InvocationTargetException e) { + fail(method.getDeclaringClass().getName() + " failed", e.getCause()); + } + }); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java deleted file mode 100644 index f16681a60..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.amazonaws.encryptionsdk; - -import com.amazonaws.encryptionsdk.jce.JceMasterKeyTest; -import org.junit.runner.RunWith; -import org.junit.runners.Suite; - -import com.amazonaws.encryptionsdk.caching.CacheIdentifierTests; -import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManagerTest; -import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCacheTest; -import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCacheThreadStormTest; -import com.amazonaws.encryptionsdk.caching.NullCryptoMaterialsCacheTest; -import com.amazonaws.encryptionsdk.internal.BlockDecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.BlockEncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.CipherHandlerTest; -import com.amazonaws.encryptionsdk.internal.DecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.EncContextSerializerTest; -import com.amazonaws.encryptionsdk.internal.EncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.FrameDecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.FrameEncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.PrimitivesParserTest; -import com.amazonaws.encryptionsdk.jce.KeyStoreProviderTest; -import com.amazonaws.encryptionsdk.model.CipherBlockHeadersTest; -import com.amazonaws.encryptionsdk.model.CipherFrameHeadersTest; -import com.amazonaws.encryptionsdk.model.KeyBlobTest; -import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequestTest; -import com.amazonaws.encryptionsdk.multi.MultipleMasterKeyTest; -import com.amazonaws.encryptionsdk.kms.KMSProviderBuilderMockTests; - -@RunWith(Suite.class) -@Suite.SuiteClasses({ - BlockDecryptionHandlerTest.class, - BlockEncryptionHandlerTest.class, - CipherHandlerTest.class, - DecryptionHandlerTest.class, - EncContextSerializerTest.class, - EncryptionHandlerTest.class, - FrameDecryptionHandlerTest.class, - FrameEncryptionHandlerTest.class, - PrimitivesParserTest.class, - KeyStoreProviderTest.class, - CipherBlockHeadersTest.class, - CipherFrameHeadersTest.class, - KeyBlobTest.class, - DecryptionMaterialsRequestTest.class, - MultipleMasterKeyTest.class, - AwsCryptoTest.class, - CryptoInputStreamTest.class, - CryptoOutputStreamTest.class, - TestVectorRunner.class, - XCompatDecryptTest.class, - DefaultCryptoMaterialsManagerTest.class, - NullCryptoMaterialsCacheTest.class, - CacheIdentifierTests.class, - CachingCryptoMaterialsManagerTest.class, - LocalCryptoMaterialsCacheTest.class, - LocalCryptoMaterialsCacheThreadStormTest.class, - UtilsTest.class, - MultipleMasterKeyTest.class, - KMSProviderBuilderMockTests.class, - JceMasterKeyTest.class -}) -public class AllTestsSuite { -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java b/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java index 70fe91baf..6e404827d 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java @@ -13,8 +13,9 @@ package com.amazonaws.encryptionsdk; -import static com.amazonaws.encryptionsdk.FastTestsOnlySuite.isFastTestSuiteActive; +import static com.amazonaws.encryptionsdk.TestUtils.assertNullChecks; import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; +import static com.amazonaws.encryptionsdk.TestUtils.isFastTestsOnly; import static java.util.Collections.singletonMap; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -37,7 +38,8 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import org.junit.Assert; +import com.amazonaws.encryptionsdk.internal.TestKeyring; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import org.junit.Before; import org.junit.Test; @@ -56,11 +58,13 @@ public class AwsCryptoTest { private StaticMasterKey masterKeyProvider; + private Keyring keyring; private AwsCrypto encryptionClient_; @Before public void init() { masterKeyProvider = spy(new StaticMasterKey("testmaterial")); + keyring = spy(new TestKeyring("testmaterial")); encryptionClient_ = new AwsCrypto(); encryptionClient_.setEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256); @@ -87,6 +91,26 @@ private void doEncryptDecrypt(final CryptoAlgorithm cryptoAlg, final int byteSiz assertArrayEquals("Bad encrypt/decrypt for " + cryptoAlg, plaintextBytes, decryptedText); } + private void doEncryptDecryptWithKeyring(final CryptoAlgorithm cryptoAlg, final int byteSize, final int frameSize) { + final byte[] plaintextBytes = new byte[byteSize]; + + final Map encryptionContext = new HashMap<>(1); + encryptionContext.put("ENC1", "Encrypt-decrypt-keyring test with %d" + byteSize); + + encryptionClient_.setEncryptionAlgorithm(cryptoAlg); + encryptionClient_.setEncryptionFrameSize(frameSize); + + final byte[] cipherText = encryptionClient_.encrypt(request -> request + .keyring(keyring) + .encryptionContext(encryptionContext) + .plaintext(plaintextBytes)).getResult(); + final byte[] decryptedText = encryptionClient_.decrypt(request -> request + .keyring(keyring) + .ciphertext(cipherText)).getResult(); + + assertArrayEquals("Bad encrypt/decrypt for " + cryptoAlg, plaintextBytes, decryptedText); + } + private void doTamperedEncryptDecrypt(final CryptoAlgorithm cryptoAlg, final int byteSize, final int frameSize) { final byte[] plaintextBytes = new byte[byteSize]; @@ -101,15 +125,29 @@ private void doTamperedEncryptDecrypt(final CryptoAlgorithm cryptoAlg, final int plaintextBytes, encryptionContext).getResult(); cipherText[cipherText.length - 2] ^= (byte) 0xff; - try { - encryptionClient_.decryptData( - masterKeyProvider, - cipherText - ).getResult(); - Assert.fail("Expected BadCiphertextException"); - } catch (final BadCiphertextException ex) { - // Expected exception - } + + assertThrows(BadCiphertextException.class, () -> encryptionClient_.decryptData( + masterKeyProvider, + cipherText)); + } + + private void doTamperedEncryptDecryptWithKeyring(final CryptoAlgorithm cryptoAlg, final int byteSize, final int frameSize) { + final byte[] plaintextBytes = new byte[byteSize]; + + final Map encryptionContext = new HashMap<>(1); + encryptionContext.put("ENC1", "Encrypt-decrypt-keyring test with %d" + byteSize); + + encryptionClient_.setEncryptionAlgorithm(cryptoAlg); + encryptionClient_.setEncryptionFrameSize(frameSize); + + final byte[] cipherText = encryptionClient_.encrypt(EncryptRequest.builder() + .keyring(keyring) + .plaintext(plaintextBytes).build()).getResult(); + cipherText[cipherText.length - 2] ^= (byte) 0xff; + + assertThrows(BadCiphertextException.class, () -> encryptionClient_.decrypt(DecryptRequest.builder() + .keyring(keyring) + .ciphertext(cipherText).build())); } private void doEncryptDecryptWithParsedCiphertext(final int byteSize, final int frameSize) { @@ -155,12 +193,13 @@ public void encryptDecrypt() { for (int j = 0; j < bytesToTest.length; j++) { final int byteSize = bytesToTest[j]; - if (byteSize > 500_000 && isFastTestSuiteActive()) { + if (byteSize > 500_000 && isFastTestsOnly()) { continue; } if (byteSize >= 0) { doEncryptDecrypt(cryptoAlg, byteSize, frameSize); + doEncryptDecryptWithKeyring(cryptoAlg, byteSize, frameSize); } } } @@ -183,12 +222,13 @@ public void encryptDecryptWithBadSignature() { for (int j = 0; j < bytesToTest.length; j++) { final int byteSize = bytesToTest[j]; - if (byteSize > 500_000 && isFastTestSuiteActive()) { + if (byteSize > 500_000 && isFastTestsOnly()) { continue; } if (byteSize >= 0) { doTamperedEncryptDecrypt(cryptoAlg, byteSize, frameSize); + doTamperedEncryptDecryptWithKeyring(cryptoAlg, byteSize, frameSize); } } } @@ -208,7 +248,7 @@ public void encryptDecryptWithParsedCiphertext() { for (int j = 0; j < bytesToTest.length; j++) { final int byteSize = bytesToTest[j]; - if (byteSize > 500_000 && isFastTestSuiteActive()) { + if (byteSize > 500_000 && isFastTestsOnly()) { continue; } @@ -255,6 +295,42 @@ public void encryptDecryptWithCustomManager() throws Exception { assertTrue(didDecrypt[0]); } + @Test + public void encryptDecryptWithCustomManagerWithKeyring() { + boolean[] didDecrypt = new boolean[] { false }; + + CryptoMaterialsManager manager = new CryptoMaterialsManager() { + @Override public EncryptionMaterials getMaterialsForEncrypt( + EncryptionMaterialsRequest request + ) { + request = request.toBuilder().setContext(singletonMap("foo", "bar")).build(); + + return new DefaultCryptoMaterialsManager(keyring).getMaterialsForEncrypt(request); + } + + @Override public DecryptionMaterials decryptMaterials( + DecryptionMaterialsRequest request + ) { + didDecrypt[0] = true; + return new DefaultCryptoMaterialsManager(keyring).decryptMaterials(request); + } + }; + + byte[] plaintext = new byte[100]; + + AwsCryptoResult ciphertext = encryptionClient_.encrypt(EncryptRequest.builder() + .cryptoMaterialsManager(manager) + .plaintext(plaintext).build()); + assertEquals("bar", ciphertext.getEncryptionContext().get("foo")); + + assertFalse(didDecrypt[0]); + AwsCryptoResult plaintextResult = encryptionClient_.decrypt(DecryptRequest.builder() + .cryptoMaterialsManager(manager) + .ciphertext(ciphertext.getResult()).build()); + assertArrayEquals(plaintext, plaintextResult.getResult()); + assertTrue(didDecrypt[0]); + } + @Test public void whenCustomCMMIgnoresAlgorithm_throws() throws Exception { boolean[] didDecrypt = new boolean[] { false }; @@ -304,6 +380,21 @@ public void whenDecrypting_invokesMKPOnce() throws Exception { verify(masterKeyProvider, times(1)).decryptDataKey(any(), any(), any()); } + @Test + public void whenDecrypting_invokesOnDecryptOnce() throws Exception { + byte[] data = encryptionClient_.encrypt(EncryptRequest.builder() + .keyring(keyring) + .plaintext(new byte[1]).build()).getResult(); + + reset(keyring); + + encryptionClient_.decrypt(DecryptRequest.builder() + .keyring(keyring) + .ciphertext(data).build()); + + verify(keyring, times(1)).onDecrypt(any(), any()); + } + private void doEstimateCiphertextSize(final CryptoAlgorithm cryptoAlg, final int inLen, final int frameSize) { final byte[] plaintext = TestIOUtils.generateRandomPlaintext(inLen); @@ -327,6 +418,32 @@ private void doEstimateCiphertextSize(final CryptoAlgorithm cryptoAlg, final int assertTrue(errMsg, estimatedCiphertextSize - cipherText.length <= 16); } + private void doEstimateCiphertextSizeWithKeyring(final CryptoAlgorithm cryptoAlg, final int inLen, final int frameSize) { + final byte[] plaintext = TestIOUtils.generateRandomPlaintext(inLen); + + final Map encryptionContext = new HashMap<>(1); + encryptionContext.put("ENC1", "Ciphertext size estimation test with " + inLen); + + encryptionClient_.setEncryptionAlgorithm(cryptoAlg); + encryptionClient_.setEncryptionFrameSize(frameSize); + + final long estimatedCiphertextSize = encryptionClient_.estimateCiphertextSize(EstimateCiphertextSizeRequest.builder() + .keyring(keyring) + .encryptionContext(encryptionContext) + .plaintextSize(inLen) + .build()); + final byte[] cipherText = encryptionClient_.encrypt(EncryptRequest.builder() + .keyring(keyring) + .encryptionContext(encryptionContext) + .plaintext(plaintext).build()).getResult(); + + // The estimate should be close (within 16 bytes) and never less than reality + final String errMsg = "Bad estimation for " + cryptoAlg + " expected: <" + estimatedCiphertextSize + + "> but was: <" + cipherText.length + ">"; + assertTrue(errMsg, estimatedCiphertextSize - cipherText.length >= 0); + assertTrue(errMsg, estimatedCiphertextSize - cipherText.length <= 16); + } + @Test public void estimateCiphertextSize() { for (final CryptoAlgorithm cryptoAlg : EnumSet.allOf(CryptoAlgorithm.class)) { @@ -340,12 +457,13 @@ public void estimateCiphertextSize() { for (int j = 0; j < bytesToTest.length; j++) { final int byteSize = bytesToTest[j]; - if (byteSize > 500_000 && isFastTestSuiteActive()) { + if (byteSize > 500_000 && isFastTestsOnly()) { continue; } if (byteSize >= 0) { doEstimateCiphertextSize(cryptoAlg, byteSize, frameSize); + doEstimateCiphertextSizeWithKeyring(cryptoAlg, byteSize, frameSize); } } } @@ -495,7 +613,7 @@ public void emptyEncryptionContext() { // Test that all the parameters that aren't allowed to be null (i.e. all of them) result in immediate NPEs if // invoked with null args @Test - public void assertNullChecks() throws Exception { + public void assertNullValidation() throws Exception { byte[] buf = new byte[1]; HashMap context = new HashMap<>(); MasterKeyProvider provider = masterKeyProvider; @@ -506,144 +624,177 @@ public void assertNullChecks() throws Exception { byte[] ciphertext = encryptionClient_.encryptData(cmm, buf).getResult(); String stringCiphertext = encryptionClient_.encryptString(cmm, "hello, world").getResult(); - TestUtils.assertNullChecks(encryptionClient_, "estimateCiphertextSize", + assertNullChecks(encryptionClient_, "estimateCiphertextSize", MasterKeyProvider.class, provider, Integer.TYPE, 42, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "estimateCiphertextSize", + assertNullChecks(encryptionClient_, "estimateCiphertextSize", CryptoMaterialsManager.class, cmm, Integer.TYPE, 42, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "estimateCiphertextSize", + assertNullChecks(encryptionClient_, "estimateCiphertextSize", MasterKeyProvider.class, provider, Integer.TYPE, 42 ); - TestUtils.assertNullChecks(encryptionClient_, "estimateCiphertextSize", + assertNullChecks(encryptionClient_, "estimateCiphertextSize", CryptoMaterialsManager.class, cmm, Integer.TYPE, 42 ); - - TestUtils.assertNullChecks(encryptionClient_, "encryptData", + assertNullChecks(encryptionClient_, "estimateCiphertextSize", + EstimateCiphertextSizeRequest.class, EstimateCiphertextSizeRequest.builder() + .cryptoMaterialsManager(cmm) + .plaintextSize(42).build() + ); + assertNullChecks(encryptionClient_, "encryptData", MasterKeyProvider.class, provider, byte[].class, buf, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "encryptData", + assertNullChecks(encryptionClient_, "encryptData", CryptoMaterialsManager.class, cmm, byte[].class, buf, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "encryptData", + assertNullChecks(encryptionClient_, "encryptData", MasterKeyProvider.class, provider, byte[].class, buf ); - TestUtils.assertNullChecks(encryptionClient_, "encryptData", + assertNullChecks(encryptionClient_, "encryptData", CryptoMaterialsManager.class, cmm, byte[].class, buf ); - TestUtils.assertNullChecks(encryptionClient_, "encryptString", + assertNullChecks(encryptionClient_, "encrypt", + EncryptRequest.class, EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .plaintext(buf).build() + ); + assertNullChecks(encryptionClient_, "encryptString", MasterKeyProvider.class, provider, String.class, "", Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "encryptString", + assertNullChecks(encryptionClient_, "encryptString", CryptoMaterialsManager.class, cmm, String.class, "", Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "encryptString", + assertNullChecks(encryptionClient_, "encryptString", MasterKeyProvider.class, provider, String.class, "" ); - TestUtils.assertNullChecks(encryptionClient_, "encryptString", + assertNullChecks(encryptionClient_, "encryptString", CryptoMaterialsManager.class, cmm, String.class, "" ); - TestUtils.assertNullChecks(encryptionClient_, "decryptData", + assertNullChecks(encryptionClient_, "decryptData", MasterKeyProvider.class, provider, byte[].class, ciphertext ); - TestUtils.assertNullChecks(encryptionClient_, "decryptData", + assertNullChecks(encryptionClient_, "decryptData", CryptoMaterialsManager.class, cmm, byte[].class, ciphertext ); - TestUtils.assertNullChecks(encryptionClient_, "decryptData", + assertNullChecks(encryptionClient_, "decryptData", MasterKeyProvider.class, provider, ParsedCiphertext.class, new ParsedCiphertext(ciphertext) ); - TestUtils.assertNullChecks(encryptionClient_, "decryptData", + assertNullChecks(encryptionClient_, "decryptData", CryptoMaterialsManager.class, cmm, ParsedCiphertext.class, new ParsedCiphertext(ciphertext) ); - TestUtils.assertNullChecks(encryptionClient_, "decryptString", + assertNullChecks(encryptionClient_, "decrypt", + DecryptRequest.class, DecryptRequest.builder() + .cryptoMaterialsManager(cmm) + .ciphertext(ciphertext).build() + ); + assertNullChecks(encryptionClient_, "decryptString", MasterKeyProvider.class, provider, String.class, stringCiphertext ); - TestUtils.assertNullChecks(encryptionClient_, "decryptString", + assertNullChecks(encryptionClient_, "decryptString", CryptoMaterialsManager.class, cmm, String.class, stringCiphertext ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", MasterKeyProvider.class, provider, OutputStream.class, os, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", CryptoMaterialsManager.class, cmm, OutputStream.class, os, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", MasterKeyProvider.class, provider, OutputStream.class, os ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", CryptoMaterialsManager.class, cmm, OutputStream.class, os ); - - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingOutputStream", + CreateEncryptingOutputStreamRequest.class, CreateEncryptingOutputStreamRequest.builder() + .cryptoMaterialsManager(cmm) + .outputStream(os).build() + ); + assertNullChecks(encryptionClient_, "createEncryptingStream", MasterKeyProvider.class, provider, InputStream.class, is, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", CryptoMaterialsManager.class, cmm, InputStream.class, is, Map.class, context ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", MasterKeyProvider.class, provider, InputStream.class, is ); - TestUtils.assertNullChecks(encryptionClient_, "createEncryptingStream", + assertNullChecks(encryptionClient_, "createEncryptingStream", CryptoMaterialsManager.class, cmm, InputStream.class, is ); + assertNullChecks(encryptionClient_, "createEncryptingInputStream", + CreateEncryptingInputStreamRequest.class, CreateEncryptingInputStreamRequest.builder() + .cryptoMaterialsManager(cmm) + .inputStream(is).build() + ); - TestUtils.assertNullChecks(encryptionClient_, "createDecryptingStream", + assertNullChecks(encryptionClient_, "createDecryptingStream", MasterKeyProvider.class, provider, OutputStream.class, os ); - TestUtils.assertNullChecks(encryptionClient_, "createDecryptingStream", + assertNullChecks(encryptionClient_, "createDecryptingStream", CryptoMaterialsManager.class, cmm, OutputStream.class, os ); - TestUtils.assertNullChecks(encryptionClient_, "createDecryptingStream", + assertNullChecks(encryptionClient_, "createDecryptingStream", MasterKeyProvider.class, provider, InputStream.class, is ); - TestUtils.assertNullChecks(encryptionClient_, "createDecryptingStream", + assertNullChecks(encryptionClient_, "createDecryptingStream", CryptoMaterialsManager.class, cmm, InputStream.class, is ); + assertNullChecks(encryptionClient_, "createDecryptingInputStream", + CreateDecryptingInputStreamRequest.class, CreateDecryptingInputStreamRequest.builder() + .cryptoMaterialsManager(cmm) + .inputStream(is).build() + ); + assertNullChecks(encryptionClient_, "createDecryptingOutputStream", + CreateDecryptingOutputStreamRequest.class, CreateDecryptingOutputStreamRequest.builder() + .cryptoMaterialsManager(cmm) + .outputStream(os).build() + ); } @Test diff --git a/src/test/java/com/amazonaws/encryptionsdk/CryptoInputStreamTest.java b/src/test/java/com/amazonaws/encryptionsdk/CryptoInputStreamTest.java index 6762475e6..7bca94cd9 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CryptoInputStreamTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CryptoInputStreamTest.java @@ -15,6 +15,7 @@ import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; import static com.amazonaws.encryptionsdk.TestUtils.insecureRandomBytes; +import static com.amazonaws.encryptionsdk.TestUtils.isFastTestsOnly; import static com.amazonaws.encryptionsdk.internal.TestIOUtils.getSha256Hash; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -163,7 +164,7 @@ public static Collection encryptDecryptParams() { // Our bytesToTest and readLenVals arrays tend to have the bigger numbers towards the end - we'll chop off // the last few as they take the longest and don't really add that much more coverage. int skipLastNSizes; - if (!FastTestsOnlySuite.isFastTestSuiteActive()) { + if (!isFastTestsOnly()) { skipLastNSizes = 0; } else if (firstAlgorithm) { // We'll run more tests for the first algorithm in the list - but not go quite so far as running the diff --git a/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java b/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java index 558dc88b6..f31a2849a 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java @@ -14,9 +14,9 @@ package com.amazonaws.encryptionsdk; import static com.amazonaws.encryptionsdk.AwsCrypto.getDefaultFrameSize; -import static com.amazonaws.encryptionsdk.FastTestsOnlySuite.isFastTestSuiteActive; import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; import static com.amazonaws.encryptionsdk.TestUtils.insecureRandomBytes; +import static com.amazonaws.encryptionsdk.TestUtils.isFastTestsOnly; import static com.amazonaws.encryptionsdk.TestUtils.toByteArray; import static com.amazonaws.encryptionsdk.internal.TestIOUtils.getSha256Hash; import static org.junit.Assert.assertArrayEquals; @@ -163,7 +163,7 @@ public static Collection encryptDecryptParams() { int[] bytesToTest = { 0, 1, frameSize - 1, frameSize, frameSize + 1, (int) (frameSize * 1.5), frameSize * 2, 1000000 }; - if (isFastTestSuiteActive()) { + if (isFastTestsOnly()) { // Exclude the last two sizes, as they're the slowest bytesToTest = Arrays.copyOfRange(bytesToTest, 0, bytesToTest.length - 2); } @@ -173,7 +173,7 @@ public static Collection encryptDecryptParams() { final int byteSize = bytesToTest[j]; int[] readLenVals = { byteSize - 1, byteSize, byteSize + 1, byteSize * 2, 1000000 }; - if (isFastTestSuiteActive()) { + if (isFastTestsOnly()) { // Only test one read() call buffer length in the fast tests. This greatly cuts down on // the combinatorial explosion of test cases here. readLenVals = Arrays.copyOfRange(readLenVals, 0, 1); diff --git a/src/test/java/com/amazonaws/encryptionsdk/DecryptRequestTest.java b/src/test/java/com/amazonaws/encryptionsdk/DecryptRequestTest.java new file mode 100644 index 000000000..da83deb61 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/DecryptRequestTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.internal.TestKeyring; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +class DecryptRequestTest { + + @Mock private Keyring keyring; + @Mock private ParsedCiphertext parsedCiphertext; + private static final byte[] CIPHERTEXT = new byte[]{1, 2, 3}; + + @Test + void testBothCiphertextAndParsedCiphertext() { + + assertThrows(IllegalArgumentException.class, () -> DecryptRequest.builder() + .keyring(keyring) + .ciphertext(CIPHERTEXT) + .parsedCiphertext(parsedCiphertext) + .build()); + } + + @Test + void testNeitherCiphertextOrParsedCiphertext() { + + assertThrows(IllegalArgumentException.class, () -> DecryptRequest.builder() + .keyring(keyring) + .build()); + } + + @Test + void testKeyringUsesDefaultCmm() { + + byte[] ciphertext = new AwsCrypto().encrypt(EncryptRequest.builder() + .keyring(new TestKeyring("keyId")) + .plaintext(new byte[]{4, 5, 6}) + .build()).getResult(); + + final CryptoMaterialsManager cryptoMaterialsManager = DecryptRequest.builder() + .keyring(keyring) + .ciphertext(ciphertext).build() + .cryptoMaterialsManager(); + + assertTrue(cryptoMaterialsManager instanceof DefaultCryptoMaterialsManager); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManagerTest.java b/src/test/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManagerTest.java index 9f693fe28..00fcfa063 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManagerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManagerTest.java @@ -2,12 +2,11 @@ import static com.amazonaws.encryptionsdk.multi.MultipleProviderFactory.buildMultiProvider; import static java.util.Collections.singletonMap; -import static org.hamcrest.Matchers.hasEntry; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Matchers.any; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.same; @@ -15,6 +14,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.nio.charset.StandardCharsets; import java.security.Signature; @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.function.Consumer; +import com.amazonaws.encryptionsdk.internal.TestKeyring; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import org.junit.Test; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; @@ -43,9 +45,10 @@ public class DefaultCryptoMaterialsManagerTest { private static final MasterKey mk1 = new StaticMasterKey("mk1"); private static final MasterKey mk2 = new StaticMasterKey("mk2"); + private static final Keyring keyring1 = new TestKeyring("keyring1"); @Test - public void encrypt_testBasicFunctionality() throws Exception { + public void encrypt_testBasicFunctionalityWithMkp() { EncryptionMaterialsRequest req = EncryptionMaterialsRequest.newBuilder().build(); EncryptionMaterials result = new DefaultCryptoMaterialsManager(mk1).getMaterialsForEncrypt(req); @@ -58,7 +61,20 @@ public void encrypt_testBasicFunctionality() throws Exception { } @Test - public void encrypt_noSignatureKeyOnUnsignedAlgo() throws Exception { + public void encrypt_testBasicFunctionalityWithKeyring() { + EncryptionMaterialsRequest req = EncryptionMaterialsRequest.newBuilder().build(); + EncryptionMaterials result = new DefaultCryptoMaterialsManager(keyring1).getMaterialsForEncrypt(req); + + assertNotNull(result.getAlgorithm()); + assertNotNull(result.getCleartextDataKey()); + assertNotNull(result.getEncryptionContext()); + assertNotNull(result.getKeyringTrace()); + assertEquals(1, result.getEncryptedDataKeys().size()); + assertEquals(0, result.getMasterKeys().size()); + } + + @Test + public void encrypt_noSignatureKeyOnUnsignedAlgoWithMkp() { CryptoAlgorithm[] algorithms = new CryptoAlgorithm[] { CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256, CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_NO_KDF, @@ -80,7 +96,29 @@ public void encrypt_noSignatureKeyOnUnsignedAlgo() throws Exception { } @Test - public void encrypt_hasSignatureKeyForSignedAlgo() throws Exception { + public void encrypt_noSignatureKeyOnUnsignedAlgoWithKeyring() { + CryptoAlgorithm[] algorithms = new CryptoAlgorithm[] { + CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256, + CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_NO_KDF, + CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA256, + CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_NO_KDF, + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256, + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_NO_KDF + }; + + for (CryptoAlgorithm algo : algorithms) { + EncryptionMaterialsRequest req + = EncryptionMaterialsRequest.newBuilder().setRequestedAlgorithm(algo).build(); + EncryptionMaterials result = new DefaultCryptoMaterialsManager(keyring1).getMaterialsForEncrypt(req); + + assertNull(result.getTrailingSignatureKey()); + assertEquals(0, result.getEncryptionContext().size()); + assertEquals(algo, result.getAlgorithm()); + } + } + + @Test + public void encrypt_hasSignatureKeyForSignedAlgoWithMkp() { CryptoAlgorithm[] algorithms = new CryptoAlgorithm[] { CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, @@ -93,6 +131,28 @@ public void encrypt_hasSignatureKeyForSignedAlgo() throws Exception { = EncryptionMaterialsRequest.newBuilder().setRequestedAlgorithm(algo).build(); EncryptionMaterials result = new DefaultCryptoMaterialsManager(mk1).getMaterialsForEncrypt(req); + + assertNotNull(result.getTrailingSignatureKey()); + assertEquals(1, result.getEncryptionContext().size()); + assertNotNull(result.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD)); + assertEquals(algo, result.getAlgorithm()); + } + } + + @Test + public void encrypt_hasSignatureKeyForSignedAlgoWithKeyring() { + CryptoAlgorithm[] algorithms = new CryptoAlgorithm[] { + CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, + CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384 + }; + + for (CryptoAlgorithm algo : algorithms) { + + EncryptionMaterialsRequest req + = EncryptionMaterialsRequest.newBuilder().setRequestedAlgorithm(algo).build(); + EncryptionMaterials result = new DefaultCryptoMaterialsManager(keyring1).getMaterialsForEncrypt(req); + assertNotNull(result.getTrailingSignatureKey()); assertEquals(1, result.getEncryptionContext().size()); assertNotNull(result.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD)); @@ -101,7 +161,7 @@ public void encrypt_hasSignatureKeyForSignedAlgo() throws Exception { } @Test - public void encrypt_dispatchesMultipleMasterKeys() throws Exception { + public void encrypt_dispatchesMultipleMasterKeys() { MasterKey mk1_spy = spy(mk1); MasterKey mk2_spy = spy(mk2); @@ -145,7 +205,7 @@ public void encrypt_dispatchesMultipleMasterKeys() throws Exception { } @Test - public void encrypt_forwardsPlaintextWhenAvailable() throws Exception { + public void encrypt_forwardsPlaintextWhenAvailable() { MasterKey mk1_spy = spy(mk1); EncryptionMaterialsRequest request = EncryptionMaterialsRequest.newBuilder() @@ -161,7 +221,7 @@ public void encrypt_forwardsPlaintextWhenAvailable() throws Exception { } @Test - public void encrypt_forwardsPlaintextSizeWhenAvailable() throws Exception { + public void encrypt_forwardsPlaintextSizeWhenAvailable() { MasterKey mk1_spy = spy(mk1); EncryptionMaterialsRequest request = EncryptionMaterialsRequest.newBuilder() @@ -177,7 +237,7 @@ public void encrypt_forwardsPlaintextSizeWhenAvailable() throws Exception { } @Test - public void encrypt_setsStreamingWhenNoSizeAvailable() throws Exception { + public void encrypt_setsStreamingWhenNoSizeAvailable() { MasterKey mk1_spy = spy(mk1); EncryptionMaterialsRequest request = EncryptionMaterialsRequest.newBuilder().build(); @@ -189,7 +249,7 @@ public void encrypt_setsStreamingWhenNoSizeAvailable() throws Exception { } @Test(expected = IllegalArgumentException.class) - public void encrypt_whenECContextKeyPresent_throws() throws Exception { + public void encrypt_whenECContextKeyPresent_throws() { EncryptionMaterialsRequest req = EncryptionMaterialsRequest.newBuilder() .setRequestedAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384) .setContext(singletonMap(Constants.EC_PUBLIC_KEY_FIELD, "some EC key")) @@ -199,7 +259,7 @@ public void encrypt_whenECContextKeyPresent_throws() throws Exception { } @Test(expected = IllegalArgumentException.class) - public void encrypt_whenNoMasterKeys_throws() throws Exception { + public void encrypt_whenNoMasterKeys_throws() { EncryptionMaterialsRequest req = EncryptionMaterialsRequest.newBuilder().build(); new DefaultCryptoMaterialsManager(new MasterKeyProvider() { @@ -218,7 +278,7 @@ public void encrypt_whenNoMasterKeys_throws() throws Exception { @Override public DataKey decryptDataKey( CryptoAlgorithm algorithm, Collection encryptedDataKeys, Map encryptionContext - ) throws UnsupportedProviderException, AwsCryptoException { + ) throws AwsCryptoException { return null; } }).getMaterialsForEncrypt(req); @@ -232,6 +292,14 @@ private EncryptionMaterials easyGenMaterials(Consumer customizer) { + EncryptionMaterialsRequest.Builder request = EncryptionMaterialsRequest.newBuilder(); + + customizer.accept(request); + + return new DefaultCryptoMaterialsManager(keyring1).getMaterialsForEncrypt(request.build()); + } + private DecryptionMaterialsRequest decryptReqFromMaterials(EncryptionMaterials result) { return DecryptionMaterialsRequest.newBuilder() .setEncryptionContext(result.getEncryptionContext()) @@ -275,15 +343,62 @@ public void decrypt_testSimpleRoundTrip() throws Exception { } } + @Test + public void decrypt_testSimpleRoundTripWithKeyring() throws Exception { + for (CryptoAlgorithm algorithm : CryptoAlgorithm.values()) { + EncryptionMaterials encryptMaterials = easyGenMaterialsForKeyring( + builder -> builder.setRequestedAlgorithm(algorithm) + ); + + DecryptionMaterials decryptMaterials + = new DefaultCryptoMaterialsManager(keyring1).decryptMaterials(decryptReqFromMaterials(encryptMaterials)); + + assertArrayEquals(decryptMaterials.getDataKey().getKey().getEncoded(), + encryptMaterials.getCleartextDataKey().getEncoded()); + + if (encryptMaterials.getTrailingSignatureKey() == null) { + assertNull(decryptMaterials.getTrailingSignatureKey()); + } else { + Signature sig = Signature.getInstance( + TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).getHashAndSignAlgorithm() + ); + + sig.initSign(encryptMaterials.getTrailingSignatureKey()); + + byte[] data = "hello world".getBytes(StandardCharsets.UTF_8); + + sig.update(data); + byte[] signature = sig.sign(); + + sig.initVerify(decryptMaterials.getTrailingSignatureKey()); + + sig.update(data); + sig.verify(signature); + } + } + } + @Test(expected = CannotUnwrapDataKeyException.class) - public void decrypt_onDecryptFailure() throws Exception { + public void decrypt_onDecryptFailure() { new DefaultCryptoMaterialsManager(mock(MasterKeyProvider.class)).decryptMaterials( decryptReqFromMaterials(easyGenMaterials(ignored -> {})) ); } + @Test(expected = CannotUnwrapDataKeyException.class) + public void decrypt_onDecryptFailureForKeyring() { + Keyring keyring = mock(Keyring.class); + DecryptionMaterials decryptionMaterials = mock(DecryptionMaterials.class); + when(keyring.onDecrypt(any(), any())).thenReturn(decryptionMaterials); + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false); + + new DefaultCryptoMaterialsManager(keyring).decryptMaterials( + decryptReqFromMaterials(easyGenMaterials(ignored -> {})) + ); + } + @Test - public void decrypt_whenTrailingSigMissing_throwsException() throws Exception { + public void decrypt_whenTrailingSigMissing_throwsException() { for (CryptoAlgorithm algorithm : CryptoAlgorithm.values()) { if (algorithm.getTrailingSignatureLength() == 0) { continue; @@ -299,13 +414,30 @@ public void decrypt_whenTrailingSigMissing_throwsException() throws Exception { .setEncryptionContext(Collections.emptyMap()) .build(); - try { - new DefaultCryptoMaterialsManager(mk1).decryptMaterials(request); - fail("expected exception"); - } catch (AwsCryptoException e) { - // ok + assertThrows(AwsCryptoException.class, () -> + new DefaultCryptoMaterialsManager(mk1).decryptMaterials(request)); + } + } + + @Test + public void decrypt_whenTrailingSigMissingForKeyring_throwsException() { + for (CryptoAlgorithm algorithm : CryptoAlgorithm.values()) { + if (algorithm.getTrailingSignatureLength() == 0) { continue; } + + EncryptionMaterials encryptMaterials = easyGenMaterialsForKeyring( + builder -> builder.setRequestedAlgorithm(algorithm) + ); + + DecryptionMaterialsRequest request = DecryptionMaterialsRequest.newBuilder() + .setEncryptedDataKeys(encryptMaterials.getEncryptedDataKeys()) + .setAlgorithm(algorithm) + .setEncryptionContext(Collections.emptyMap()) + .build(); + + assertThrows(AwsCryptoException.class, () -> + new DefaultCryptoMaterialsManager(keyring1).decryptMaterials(request)); } } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/EncryptRequestTest.java b/src/test/java/com/amazonaws/encryptionsdk/EncryptRequestTest.java new file mode 100644 index 000000000..564b017e1 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/EncryptRequestTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +class EncryptRequestTest { + + @Mock private Keyring keyring; + @Mock private CryptoMaterialsManager cmm; + private static final byte[] PLAINTEXT = new byte[]{1, 2, 3}; + + @Test + void testBothCmmAndKeyring() { + + assertThrows(IllegalArgumentException.class, () -> EncryptRequest.builder() + .cryptoMaterialsManager(cmm) + .keyring(keyring) + .plaintext(PLAINTEXT) + .build()); + } + + @Test + void testNeitherCmmOrKeyring() { + + assertThrows(IllegalArgumentException.class, () -> EncryptRequest.builder() + .plaintext(PLAINTEXT) + .build()); + } + + @Test + void testKeyringUsesDefaultCmm() { + + assertTrue(EncryptRequest.builder() + .keyring(keyring) + .plaintext(PLAINTEXT).build().cryptoMaterialsManager() + instanceof DefaultCryptoMaterialsManager); + } + + @Test + void testNoEncryptionContext() { + + assertEquals(0, EncryptRequest.builder() + .plaintext(PLAINTEXT) + .keyring(keyring).build().encryptionContext().size()); + } + + @Test + void testNullPlaintext() { + assertThrows(NullPointerException.class, () -> EncryptRequest.builder() + .keyring(keyring).build()); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/FastTestsOnlySuite.java b/src/test/java/com/amazonaws/encryptionsdk/FastTestsOnlySuite.java deleted file mode 100644 index 528ae273e..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/FastTestsOnlySuite.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.amazonaws.encryptionsdk; - -import java.util.concurrent.TimeUnit; - -import org.junit.ClassRule; -import org.junit.experimental.categories.Categories; -import org.junit.rules.TestRule; -import org.junit.rules.Timeout; -import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.junit.runner.Runner; -import org.junit.runners.Suite; -import org.junit.runners.model.InitializationError; -import org.junit.runners.model.RunnerBuilder; -import org.junit.runners.model.Statement; - -/** - * This test suite is intended to assist in rapid development; it filters out some of the slower, more exhaustive tests - * in the overall test suite to allow for a rapid edit-test cycle. - */ -@RunWith(FastTestsOnlySuite.CustomRunner.class) -@Suite.SuiteClasses({ - AllTestsSuite.class -}) -@Categories.ExcludeCategory(SlowTestCategory.class) -public class FastTestsOnlySuite { - private static InheritableThreadLocal IS_FAST_TEST_SUITE_ACTIVE = new InheritableThreadLocal() { - @Override protected Boolean initialValue() { - return false; - } - }; - - // This method is used to adjust DataProviders to provide a smaller subset of their test cases when the fast tests - // are selected - public static boolean isFastTestSuiteActive() { - return IS_FAST_TEST_SUITE_ACTIVE.get(); - } - - // Require that this fast suite completes relatively quickly. If you're seeing this timeout get hit, it's time to - // pare down tests some more. As a general rule of thumb, we should avoid any single test taking more than 10s, and - // try to keep the number of such slow tests to a minimum. - @ClassRule - public static Timeout timeout = new Timeout(2, TimeUnit.MINUTES); - - @ClassRule - public static EnableFastSuite enableFastSuite = new EnableFastSuite(); - - // TestRules run over the execution of tests, but not over the generation of parameterized test data... - private static class EnableFastSuite implements TestRule { - @Override public Statement apply( - Statement base, Description description - ) { - return new Statement() { - @Override public void evaluate() throws Throwable { - Boolean oldValue = IS_FAST_TEST_SUITE_ACTIVE.get(); - - try { - IS_FAST_TEST_SUITE_ACTIVE.set(true); - base.evaluate(); - } finally { - IS_FAST_TEST_SUITE_ACTIVE.set(oldValue); - } - } - }; - } - } - - // ... so we also need a custom TestRunner that will pass the flag on to the parameterized test data generators. - public static class CustomRunner extends Categories { - public CustomRunner(Class klass, RunnerBuilder builder) throws InitializationError { - super( - klass, - new RunnerBuilder() { - @Override public Runner runnerForClass(Class testClass) throws Throwable { - Boolean oldValue = IS_FAST_TEST_SUITE_ACTIVE.get(); - - try { - IS_FAST_TEST_SUITE_ACTIVE.set(true); - Runner r = builder.runnerForClass(testClass); - return r; - } finally { - IS_FAST_TEST_SUITE_ACTIVE.set(oldValue); - } - } - } - ); - } - } -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java b/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java deleted file mode 100644 index 44628b0db..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.amazonaws.encryptionsdk; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; - -import com.amazonaws.encryptionsdk.kms.KMSProviderBuilderIntegrationTests; -import com.amazonaws.encryptionsdk.kms.XCompatKmsDecryptTest; - -@RunWith(Suite.class) -@Suite.SuiteClasses({ - XCompatKmsDecryptTest.class, - KMSProviderBuilderIntegrationTests.class -}) -public class IntegrationTestSuite { -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/SlowTestCategory.java b/src/test/java/com/amazonaws/encryptionsdk/SlowTestCategory.java deleted file mode 100644 index 9814b0948..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/SlowTestCategory.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.amazonaws.encryptionsdk; - -/** - * JUnit category marking tests to be excluded from the FastTestsOnlySuite. Usage: - * - * @Category(SlowTestCategory.class) - * @Test - * public void mySlowTest() { - * // encrypt a couple terabytes of test data - * } - * - * - */ -public interface SlowTestCategory {} diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java b/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java index 6763798f9..047189918 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java +++ b/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java @@ -34,6 +34,16 @@ private static byte[] ensureRandomCached(int length) { }); } + /** + * JUnit tag name marking tests or test classes to be excluded from normal test profiles + * and only run in an ad hoc manner. + */ + public static final String TAG_AD_HOC = "ad_hoc"; + /** + * JUnit tag name marking integration tests + */ + public static final String TAG_INTEGRATION = "integration"; + @FunctionalInterface public interface ThrowingRunnable { void run() throws Throwable; @@ -211,4 +221,8 @@ public static int[] signedBytesToUnsignedBytes(final byte[] signedBytes) { return unsignedBytes; } + + public static boolean isFastTestsOnly() { + return Boolean.parseBoolean(System.getProperty("fastTestsOnly")); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java index 14019c2e2..89f21479b 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -1,25 +1,42 @@ -package com.amazonaws.encryptionsdk; +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ -import static java.lang.String.format; +package com.amazonaws.encryptionsdk; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; +import com.amazonaws.encryptionsdk.keyrings.StandardKeyrings; +import com.amazonaws.encryptionsdk.kms.AwsKmsClientSupplier; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers; import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; import com.amazonaws.util.IOUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.bouncycastle.util.encoders.Base64; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.JarURLConnection; import java.net.URL; import java.security.GeneralSecurityException; @@ -38,32 +55,40 @@ import java.util.jar.JarFile; import java.util.zip.ZipEntry; -@RunWith(Parameterized.class) -public class TestVectorRunner { - // We save the files in memory to avoid repeatedly retrieving them. This won't work if the plaintexts are too - // large or numerous +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static org.apache.commons.lang3.Validate.isTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +@Tag(TestUtils.TAG_INTEGRATION) +class TestVectorRunner { + // We save the files in memory to avoid repeatedly retrieving them. + // This won't work if the plaintexts are too large or numerous private static final Map cachedData = new HashMap<>(); - - private final String testName; - private final TestCase testCase; - - public TestVectorRunner(final String testName, TestCase testCase) { - this.testName = testName; - this.testCase = testCase; - } - - @Test - public void decrypt() { + private static final AwsKmsClientSupplier awsKmsClientSupplier = StandardAwsKmsClientSuppliers.defaultBuilder() + .credentialsProvider(new DefaultAWSCredentialsProviderChain()) + .build(); + private static final KmsMasterKeyProvider kmsProv = KmsMasterKeyProvider + .builder() + .withCustomClientFactory(awsKmsClientSupplier::getClient) + .build(); + + @ParameterizedTest(name = "Compatibility Test: {0}") + @MethodSource("data") + void decrypt(TestCase testCase) { AwsCrypto crypto = new AwsCrypto(); - byte[] plaintext = crypto.decryptData(testCase.mkp, cachedData.get(testCase.ciphertextPath)).getResult(); + byte[] keyringPlaintext = crypto.decrypt(DecryptRequest.builder() + .ciphertext(cachedData.get(testCase.ciphertextPath)) + .keyring(testCase.keyring).build()).getResult(); + byte[] mkpPlaintext = crypto.decryptData(testCase.mkp, cachedData.get(testCase.ciphertextPath)).getResult(); final byte[] expectedPlaintext = cachedData.get(testCase.plaintextPath); - Assert.assertArrayEquals(expectedPlaintext, plaintext); + assertArrayEquals(expectedPlaintext, keyringPlaintext); + assertArrayEquals(expectedPlaintext, mkpPlaintext); } - @Parameterized.Parameters(name="Compatibility Test: {0}") @SuppressWarnings("unchecked") - public static Collection data() throws Exception { + static Collection data() throws Exception { final String zipPath = System.getProperty("testVectorZip"); if (zipPath == null) { return Collections.emptyList(); @@ -73,43 +98,33 @@ public static Collection data() throws Exception { try (JarFile jar = jarConnection.getJarFile()) { final Map manifest = readJsonMapFromJar(jar, "manifest.json"); - final Map metaData = (Map) manifest.get("manifest"); // We only support "awses-decrypt" type manifests right now - if (!"awses-decrypt".equals(metaData.get("type"))) { - throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); - } - - if (!Integer.valueOf(1).equals(metaData.get("version"))) { - throw new IllegalArgumentException("Unsupported manifest version: " + metaData.get("version")); - } + isTrue("awses-decrypt".equals(metaData.get("type")), "Unsupported manifest type: %s", metaData.get("type")); + isTrue(Integer.valueOf(1).equals(metaData.get("version")), "Unsupported manifest version: %s", metaData.get("version")); final Map keys = parseKeyManifest(readJsonMapFromJar(jar, (String) manifest.get("keys"))); - final KmsMasterKeyProvider kmsProv = KmsMasterKeyProvider - .builder() - .withCredentials(new DefaultAWSCredentialsProviderChain()) - .build(); + final List testCases = new ArrayList<>(); + + ((Map>) manifest.get("tests")).forEach( + (testName, data) -> testCases.add(parseTest(testName, data, keys, jar))); - List testCases = new ArrayList<>(); - for (Map.Entry> testEntry : - ((Map>) manifest.get("tests")).entrySet()) { - testCases.add(new Object[]{testEntry.getKey(), - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProv)}); - } return testCases; } } - @AfterClass - public static void teardown() { + @AfterAll + static void teardown() { cachedData.clear(); } - private static byte[] readBytesFromJar(JarFile jar, String fileName) throws IOException { + private static byte[] readBytesFromJar(JarFile jar, String fileName) { try (InputStream is = readFromJar(jar, fileName)) { return IOUtils.toByteArray(is); + } catch (IOException ex) { + throw new UncheckedIOException(ex); } } @@ -126,21 +141,15 @@ private static InputStream readFromJar(JarFile jar, String name) throws IOExcept return jar.getInputStream(entry); } - private static void cacheData(JarFile jar, String url) throws IOException { - if (!cachedData.containsKey(url)) { - cachedData.put(url, readBytesFromJar(jar, url)); - } - } - @SuppressWarnings("unchecked") private static TestCase parseTest(String testName, Map data, Map keys, - JarFile jar, KmsMasterKeyProvider kmsProv) throws IOException { + JarFile jar) { final String plaintextUrl = (String) data.get("plaintext"); - cacheData(jar, plaintextUrl); final String ciphertextURL = (String) data.get("ciphertext"); - cacheData(jar, ciphertextURL); + cachedData.computeIfAbsent(plaintextUrl, k -> readBytesFromJar(jar, k)); + cachedData.computeIfAbsent(ciphertextURL, k -> readBytesFromJar(jar, k)); - @SuppressWarnings("generic") + final List keyrings = new ArrayList<>(); final List> mks = new ArrayList<>(); for (Map mkEntry : (List>) data.get("master-keys")) { @@ -149,35 +158,61 @@ private static TestCase parseTest(String testName, Map data, Map final KeyEntry key = keys.get(keyName); if ("aws-kms".equals(type)) { + keyrings.add(StandardKeyrings.awsKmsBuilder() + .awsKmsClientSupplier(awsKmsClientSupplier) + .generatorKeyId(AwsKmsCmkId.fromString(key.keyId)) + .build()); mks.add(kmsProv.getMasterKey(key.keyId)); } else if ("raw".equals(type)) { final String provId = mkEntry.get("provider-id"); final String algorithm = mkEntry.get("encryption-algorithm"); if ("aes".equals(algorithm)) { + keyrings.add(StandardKeyrings.rawAesBuilder() + .keyName(key.keyId) + .keyNamespace(provId) + .wrappingKey((SecretKey) key.key).build()); mks.add(JceMasterKey.getInstance((SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; + final RsaPaddingScheme paddingScheme; final String padding = mkEntry.get("padding-algorithm"); if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; + paddingScheme = RsaPaddingScheme.PKCS1; } else if ("oaep-mgf1".equals(padding)) { - final String hashName = mkEntry.get("padding-hash") - .replace("sha", "sha-") - .toUpperCase(); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + switch(mkEntry.get("padding-hash")) { + case "sha1": + paddingScheme = RsaPaddingScheme.OAEP_SHA1_MGF1; + break; + case "sha256": + paddingScheme = RsaPaddingScheme.OAEP_SHA256_MGF1; + break; + case "sha384": + paddingScheme = RsaPaddingScheme.OAEP_SHA384_MGF1; + break; + case "sha512": + paddingScheme = RsaPaddingScheme.OAEP_SHA512_MGF1; + break; + default: + throw new IllegalArgumentException("Unsupported padding hash:" + mkEntry.get("padding-hash")); + } } else { throw new IllegalArgumentException("Unsupported padding:" + padding); } final PublicKey wrappingKey; final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { + if (key.key instanceof PublicKey) { wrappingKey = (PublicKey) key.key; unwrappingKey = null; } else { wrappingKey = null; unwrappingKey = (PrivateKey) key.key; } - mks.add(JceMasterKey.getInstance(wrappingKey, unwrappingKey, provId, key.keyId, transformation)); + keyrings.add(StandardKeyrings.rawRsaBuilder() + .publicKey(wrappingKey) + .privateKey(unwrappingKey) + .keyNamespace(provId) + .keyName(key.keyId) + .paddingScheme(paddingScheme).build()); + mks.add(JceMasterKey.getInstance(wrappingKey, unwrappingKey, provId, key.keyId, paddingScheme.getTransformation())); } else { throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); } @@ -186,11 +221,11 @@ private static TestCase parseTest(String testName, Map data, Map } } - return new TestCase(testName, ciphertextURL, plaintextUrl, mks); + return new TestCase(testName, ciphertextURL, plaintextUrl, keyrings, mks); } @SuppressWarnings("unchecked") - private static Map parseKeyManifest(final Map keysManifest) throws GeneralSecurityException { + static Map parseKeyManifest(final Map keysManifest) throws GeneralSecurityException { // check our type final Map metaData = (Map) keysManifest.get("manifest"); if (!"keys".equals(metaData.get("type"))) { @@ -221,8 +256,7 @@ private static Map parseKeyManifest(final Map if (!"base64".equals(encoding)) { throw new IllegalArgumentException(format("Key %s is symmetric but has encoding %s", keyId, encoding)); } - keyEntry = new KeyEntry(name, keyId, keyType, - new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); + keyEntry = new KeyEntry(keyId, new SecretKeySpec(Base64.decode(material), algorithm.toUpperCase())); break; case "private": kf = KeyFactory.getInstance(algorithm); @@ -230,8 +264,7 @@ private static Map parseKeyManifest(final Map throw new IllegalArgumentException(format("Key %s is private but has encoding %s", keyId, encoding)); } byte[] pkcs8Key = parsePem(material); - keyEntry = new KeyEntry(name, keyId, keyType, - kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); + keyEntry = new KeyEntry(keyId, kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key))); break; case "public": kf = KeyFactory.getInstance(algorithm); @@ -239,11 +272,10 @@ private static Map parseKeyManifest(final Map throw new IllegalArgumentException(format("Key %s is private but has encoding %s", keyId, encoding)); } byte[] x509Key = parsePem(material); - keyEntry = new KeyEntry(name, keyId, keyType, - kf.generatePublic(new X509EncodedKeySpec(x509Key))); + keyEntry = new KeyEntry(keyId, kf.generatePublic(new X509EncodedKeySpec(x509Key))); break; case "aws-kms": - keyEntry = new KeyEntry(name, keyId, keyType, null); + keyEntry = new KeyEntry(keyId, null); break; default: throw new IllegalArgumentException("Unsupported key type: " + keyType); @@ -261,15 +293,11 @@ private static byte[] parsePem(String pem) { } private static class KeyEntry { - final String name; final String keyId; - final String type; final Key key; - private KeyEntry(String name, String keyId, String type, Key key) { - this.name = name; + private KeyEntry(String keyId, Key key) { this.keyId = keyId; - this.type = type; this.key = key; } } @@ -278,17 +306,20 @@ private static class TestCase { private final String name; private final String ciphertextPath; private final String plaintextPath; + private final Keyring keyring; private final MasterKeyProvider mkp; - private TestCase(String name, String ciphertextPath, String plaintextPath, List> mks) { - this(name, ciphertextPath, plaintextPath, MultipleProviderFactory.buildMultiProvider(mks)); - } - - private TestCase(String name, String ciphertextPath, String plaintextPath, MasterKeyProvider mkp) { + private TestCase(String name, String ciphertextPath, String plaintextPath, List keyrings, List> mks) { this.name = name; this.ciphertextPath = ciphertextPath; this.plaintextPath = plaintextPath; - this.mkp = mkp; + this.keyring = StandardKeyrings.multi(keyrings.get(0), keyrings.size() > 1 ? keyrings.subList(1, keyrings.size()) : emptyList()); + this.mkp = MultipleProviderFactory.buildMultiProvider(mks); + } + + @Override + public String toString() { + return name; } } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/XCompatDecryptTest.java b/src/test/java/com/amazonaws/encryptionsdk/XCompatDecryptTest.java deleted file mode 100644 index 9e7a278a4..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/XCompatDecryptTest.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.amazonaws.encryptionsdk; - -import java.io.File; -import java.io.StringReader; -import java.lang.IllegalArgumentException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.crypto.spec.SecretKeySpec; - -import org.apache.commons.lang3.StringUtils; - -import org.bouncycastle.util.io.pem.PemReader; - -import static org.junit.Assert.assertArrayEquals; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import com.amazonaws.encryptionsdk.internal.Utils; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.core.type.TypeReference; - -import com.amazonaws.encryptionsdk.jce.JceMasterKey; -import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; - -@RunWith(Parameterized.class) -public class XCompatDecryptTest { - private static final String STATIC_XCOMPAT_NAME = "static-aws-xcompat"; - private static final String AES_GCM = "AES/GCM/NoPadding"; - - private String plaintextFileName; - private String ciphertextFileName; - private MasterKeyProvider masterKeyProvider; - - public XCompatDecryptTest( - String plaintextFileName, - String ciphertextFileName, - MasterKeyProvider masterKeyProvider - ) throws Exception { - this.plaintextFileName = plaintextFileName; - this.ciphertextFileName = ciphertextFileName; - this.masterKeyProvider = masterKeyProvider; - } - - @Parameters(name="{index}: testDecryptFromFile({0}, {1}, {2})") - public static Collection data() throws Exception{ - String baseDirName; - baseDirName = System.getProperty("staticCompatibilityResourcesDir"); - if (baseDirName == null) { - baseDirName = - XCompatDecryptTest.class.getProtectionDomain().getCodeSource().getLocation().getPath() + - "aws_encryption_sdk_resources"; - } - - List testCases_ = new ArrayList(); - - String ciphertextManifestName = StringUtils.join( - new String[]{ - baseDirName, - "manifests", - "ciphertext.manifest" - }, - File.separator - ); - File ciphertextManifestFile = new File(ciphertextManifestName); - - if (!ciphertextManifestFile.exists()) { - return Collections.emptySet(); - } - - ObjectMapper ciphertextManifestMapper = new ObjectMapper(); - Map ciphertextManifest = ciphertextManifestMapper.readValue( - ciphertextManifestFile, - new TypeReference>(){} - ); - - HashMap> staticKeyMap = new HashMap>(); - - Map testKeys = (Map)ciphertextManifest.get("test_keys"); - - for (Map.Entry keyType : testKeys.entrySet()) { - Map keys = (Map)keyType.getValue(); - HashMap thisKeyType = new HashMap(); - for (Map.Entry key : keys.entrySet()) { - Map thisKey = (Map)key.getValue(); - String keyRaw = new String( - StringUtils.join( - (List)thisKey.get("key"), - (String)thisKey.getOrDefault("line_separator", "") - ).getBytes(), - StandardCharsets.UTF_8 - ); - byte[] keyBytes; - switch ((String)thisKey.get("encoding")) { - case "base64": - keyBytes = Utils.decodeBase64String(keyRaw); - break; - case "pem": - PemReader pemReader = new PemReader(new StringReader(keyRaw)); - keyBytes = pemReader.readPemObject().getContent(); - break; - case "raw": - default: - keyBytes = keyRaw.getBytes(); - } - thisKeyType.put((String)key.getKey(), keyBytes); - } - staticKeyMap.put((String)keyType.getKey(), thisKeyType); - } - - final KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA"); - - List> testCases = (List>)ciphertextManifest.get("test_cases"); - for (Map testCase : testCases) { - Map plaintext = (Map)testCase.get("plaintext"); - Map ciphertext = (Map)testCase.get("ciphertext"); - - short algId = (short) Integer.parseInt((String)testCase.get("algorithm"), 16); - CryptoAlgorithm encryptionAlgorithm = CryptoAlgorithm.deserialize(algId); - - List> masterKeys = (List>)testCase.get("master_keys"); - List allMasterKeys = new ArrayList(); - for (Map aMasterKey : masterKeys) { - String providerId = (String)aMasterKey.get("provider_id"); - if (providerId.equals(STATIC_XCOMPAT_NAME) && (boolean)aMasterKey.get("decryptable")) { - String paddingAlgorithm = (String)aMasterKey.getOrDefault("padding_algorithm", ""); - String paddingHash = (String)aMasterKey.getOrDefault("padding_hash", ""); - Integer keyBits = (Integer)aMasterKey.getOrDefault( - "key_bits", - encryptionAlgorithm.getDataKeyLength() * 8 - ); - String keyId = - (String)aMasterKey.get("encryption_algorithm") + "." + - keyBits.toString() + "." + - paddingAlgorithm + "." + - paddingHash; - String encAlg = (String)aMasterKey.get("encryption_algorithm"); - switch (encAlg.toUpperCase()) { - case "RSA": - String cipherBase = "RSA/ECB/"; - String cipherName; - switch (paddingAlgorithm) { - case "OAEP-MGF1": - cipherName = cipherBase + "OAEPWith" + paddingHash + "AndMGF1Padding"; - break; - case "PKCS1": - cipherName = cipherBase + paddingAlgorithm + "Padding"; - break; - default: - throw new IllegalArgumentException("Unknown padding algorithm: " + paddingAlgorithm); - } - PrivateKey privKey = rsaKeyFactory.generatePrivate(new PKCS8EncodedKeySpec(staticKeyMap.get("RSA").get(keyBits.toString()))); - allMasterKeys.add(JceMasterKey.getInstance( - null, - privKey, - STATIC_XCOMPAT_NAME, - keyId, - cipherName - )); - break; - case "AES": - SecretKeySpec spec = new SecretKeySpec( - staticKeyMap.get("AES").get(keyBits.toString()), - 0, - encryptionAlgorithm.getDataKeyLength(), - encryptionAlgorithm.getDataKeyAlgo() - ); - allMasterKeys.add(JceMasterKey.getInstance(spec, STATIC_XCOMPAT_NAME, keyId, AES_GCM)); - break; - default: - throw new IllegalArgumentException("Unknown encryption algorithm: " + encAlg.toUpperCase()); - } - } - } - - if (allMasterKeys.size() > 0) { - final MasterKeyProvider provider = MultipleProviderFactory.buildMultiProvider(allMasterKeys); - testCases_.add(new Object[]{ - baseDirName + File.separator + plaintext.get("filename"), - baseDirName + File.separator + ciphertext.get("filename"), - provider - }); - } - } - return testCases_; - } - - @Test - public void testDecryptFromFile() throws Exception { - AwsCrypto crypto = new AwsCrypto(); - byte ciphertextBytes[] = Files.readAllBytes(Paths.get(ciphertextFileName)); - byte plaintextBytes[] = Files.readAllBytes(Paths.get(plaintextFileName)); - final CryptoResult decryptResult = crypto.decryptData( - masterKeyProvider, - ciphertextBytes - ); - assertArrayEquals(plaintextBytes, (byte[])decryptResult.getResult()); - } -} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java index dcb0de9cd..0393e906e 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.Map; +import com.amazonaws.encryptionsdk.keyrings.Keyring; import org.junit.Before; import org.junit.Test; @@ -29,12 +30,16 @@ import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import static org.junit.Assert.assertEquals; + public class DecryptionHandlerTest { private StaticMasterKey masterKeyProvider_; + private Keyring keyring; @Before public void init() { masterKeyProvider_ = new StaticMasterKey("testmaterial"); + keyring = new TestKeyring("testmaterial"); } @Test(expected = NullPointerException.class) @@ -140,4 +145,13 @@ public void invalidOffsetProcessBytes() { final byte[] out = new byte[1]; decryptionHandler.processBytes(in, -1, in.length, out, 0); } -} \ No newline at end of file + + @Test + public void testNullMasterKey() { + final DecryptionHandler decryptionHandler = DecryptionHandler.create(new DefaultCryptoMaterialsManager(keyring)); + final byte[] out = new byte[1]; + final byte[] testHeaders = getTestHeaders(); + decryptionHandler.processBytes(getTestHeaders(), 0, testHeaders.length, out, 0); + assertEquals(0, decryptionHandler.getMasterKeys().size()); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerVeryLongTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerVeryLongTest.java index 228bcc28f..7970d441e 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerVeryLongTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerVeryLongTest.java @@ -1,28 +1,29 @@ package com.amazonaws.encryptionsdk.internal; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.fail; - import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; +import com.amazonaws.encryptionsdk.TestUtils; import org.bouncycastle.util.encoders.Hex; -import org.bouncycastle.util.encoders.HexTranslator; -import org.junit.Test; import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.model.CipherFrameHeaders; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; /* * This test exhaustively encrypts a 2^32 frame message, which takes approximately 2-3 hours on my hardware. Because of * this long test time, this test is not run as part of the normal suites. */ -public class FrameEncryptionHandlerVeryLongTest { +@Tag(TestUtils.TAG_AD_HOC) +class FrameEncryptionHandlerVeryLongTest { @Test - public void exhaustiveIVCheck() throws Exception { + void exhaustiveIVCheck() throws Exception { CryptoAlgorithm algorithm = CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_NO_KDF; FrameEncryptionHandler frameEncryptionHandler_ = new FrameEncryptionHandler( new SecretKeySpec(new byte[16], "AES"), diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/TestKeyring.java b/src/test/java/com/amazonaws/encryptionsdk/internal/TestKeyring.java new file mode 100644 index 000000000..c01812001 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/TestKeyring.java @@ -0,0 +1,189 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.internal; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.keyrings.Keyring; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceEntry; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.List; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static java.util.Objects.requireNonNull; + +/** + * Implementation of the {@link Keyring} interface that should only + * used for unit-tests. + *

    + * Contains a statically defined asymmetric key-pair that can be used + * to encrypt and decrypt (randomly generated) symmetric data key. + *

    + * + * @author wesleyr + */ +@NotThreadSafe +public class TestKeyring implements Keyring { + private static final String PROVIDER_ID = "static_provider"; + + /** + * Encryption algorithm for the key-pair + */ + private static final String ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding"; + + /** + * Encryption algorithm for the KeyFactory + */ + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + + /** + * The ID of the key + */ + private final String keyId_; + + /** + * The {@link Cipher} object created with the public part of + * the key. It's used to encrypt data keys. + */ + private final Cipher keyEncryptionCipher_; + + /** + * The {@link Cipher} object created with the private part of + * the key. It's used to decrypt encrypted data keys. + */ + private final Cipher keyDecryptionCipher_; + + /** + * Creates a new object that encrypts the data key with a key + * whose id is {@code keyId}. + *

    + * The value of {@code keyId} does not affect how the data key will be + * generated or encrypted. The {@code keyId} forms part of the header + * of the encrypted data, and is used to ensure that the header cannot + * be tempered with. + */ + public TestKeyring(@Nonnull final String keyId) { + this.keyId_ = requireNonNull(keyId); + + try { + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + KeySpec publicKeySpec = new X509EncodedKeySpec(publicKey_v1); + PublicKey pubKey = keyFactory.generatePublic(publicKeySpec); + KeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKey_v1); + PrivateKey privKey = keyFactory.generatePrivate(privateKeySpec); + + keyEncryptionCipher_ = Cipher.getInstance(ENCRYPTION_ALGORITHM); + keyEncryptionCipher_.init(Cipher.ENCRYPT_MODE, pubKey); + + keyDecryptionCipher_ = Cipher.getInstance(ENCRYPTION_ALGORITHM); + keyDecryptionCipher_.init(Cipher.DECRYPT_MODE, privKey); + + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public EncryptionMaterials onEncrypt(EncryptionMaterials encryptionMaterials) { + if(!encryptionMaterials.hasCleartextDataKey()) { + return generateDataKey(encryptionMaterials); + } else { + byte[] encryptedKey; + try { + encryptedKey = keyEncryptionCipher_.doFinal(encryptionMaterials.getCleartextDataKey().getEncoded()); + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + return encryptionMaterials.withEncryptedDataKey(new KeyBlob(PROVIDER_ID, keyId_.getBytes(PROVIDER_ENCODING), encryptedKey), + new KeyringTraceEntry(PROVIDER_ID, keyId_, KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + } + } + + @Override + public DecryptionMaterials onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + try { + for (EncryptedDataKey edk :encryptedDataKeys) { + if (keyId_.equals(new String(edk.getProviderInformation(), StandardCharsets.UTF_8))) { + byte[] unencryptedDataKey = keyDecryptionCipher_.doFinal(edk.getEncryptedDataKey()); + SecretKey key = new SecretKeySpec(unencryptedDataKey, decryptionMaterials.getAlgorithm().getDataKeyAlgo()); + + return decryptionMaterials.withCleartextDataKey(key, + new KeyringTraceEntry(PROVIDER_ID, keyId_, KeyringTraceFlag.DECRYPTED_DATA_KEY)); + } + } + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + + return decryptionMaterials; + } + + private EncryptionMaterials generateDataKey(EncryptionMaterials encryptionMaterials) { + try { + final byte[] rawKey = new byte[encryptionMaterials.getAlgorithm().getDataKeyLength()]; + Utils.getSecureRandom().nextBytes(rawKey); + SecretKey key = new SecretKeySpec(rawKey, encryptionMaterials.getAlgorithm().getDataKeyAlgo()); + byte[] encryptedKey = keyEncryptionCipher_.doFinal(key.getEncoded()); + + return encryptionMaterials + .withCleartextDataKey(key, + new KeyringTraceEntry(PROVIDER_ID, keyId_, KeyringTraceFlag.GENERATED_DATA_KEY)) + .withEncryptedDataKey(new KeyBlob(PROVIDER_ID, keyId_.getBytes(PROVIDER_ENCODING), encryptedKey), + new KeyringTraceEntry(PROVIDER_ID, keyId_, KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + } catch (GeneralSecurityException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Statically configured private key. + */ + private static final byte[] privateKey_v1 = Utils.decodeBase64String( + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKLpwqjYtYExVilW/Hg0ogWv9xZ+" + + "THj4IzvISLlPtK8W6KXMcqukfuxdYmndPv8UD1DbdHFYSSistdqoBN32vVQOQnJZyYm45i2TDOV0" + + "M2DtHtR6aMMlBLGtdPeeaT88nQfI1ORjRDyR1byMwomvmKifZYga6FjLt/sgqfSE9BUnAgMBAAEC" + + "gYAqnewGL2qLuVRIzDCPYXVg938zqyZmHsNYyDP+BhPGGcASX0FAFW/+dQ9hkjcAk0bOaBo17Fp3" + + "AXcxE/Lx/bHY+GWZ0wOJfl3aJBVJOpW8J6kwu68BUCmuFtRgbLSFu5+fbey3pKafYSptbX1fAI+z" + + "hTx+a9B8pnn79ad4ziJ2QQJBAM+YHPGAEbr5qcNkwyy0xZgR/TLlcW2NQUt8HZpmErdX6d328iBC" + + "SPb8+whXxCXZC3Mr+35IZ1pxxf0go/zGQv0CQQDI5oH0z1CKxoT6ErswNzB0oHxq/wD5mhutyqHa" + + "mxbG5G3fN7I2IclwaXEA2eutIKxFMQNZYsX5mNYsrveSKivzAkABiujUJpZ7JDXNvObyYxmAyslt" + + "4mSYYs9UZ0S1DAMhl6amPpqIANYX98NJyZUsjtNV9MK2qoUSF/xXqDFvxG1lAkBhP5Ow2Zn3U1mT" + + "Y/XQxSZjjjwr3vyt1neHjQsEMwa3iGPXJbLSmVBVZfUZoGOBDsvVQoCIiFOlGuKyBpA45MkZAkAH" + + "ksUrS9xLrDIUOI2BzMNRsK0bH7KJ+PFxm2SBgJOF9+Uf2A9LIP4IvESZq+ufp6c8YaqgR6Id1vws" + + "7rUyGoa5"); + + /** + * Statically configured public key. + */ + private static final byte[] publicKey_v1 = Utils.decodeBase64String( + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCi6cKo2LWBMVYpVvx4NKIFr/cWfkx4+CM7yEi5" + + "T7SvFuilzHKrpH7sXWJp3T7/FA9Q23RxWEkorLXaqATd9r1UDkJyWcmJuOYtkwzldDNg7R7UemjD" + + "JQSxrXT3nmk/PJ0HyNTkY0Q8kdW8jMKJr5ion2WIGuhYy7f7IKn0hPQVJwIDAQAB"); +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringTest.java new file mode 100644 index 000000000..c295d3f9c --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/AwsKmsKeyringTest.java @@ -0,0 +1,371 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.exception.MismatchedDataKeyException; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao; +import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao.DecryptDataKeyResult; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static com.amazonaws.encryptionsdk.internal.Constants.AWS_KMS_PROVIDER_ID; +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.ENCRYPTED_DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.GENERATED_DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AwsKmsKeyringTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + private static final SecretKey PLAINTEXT_DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + private static final String GENERATOR_KEY_ID = "arn:aws:kms:us-east-1:999999999999:key/generator-89ab-cdef-fedc-ba9876543210"; + private static final String KEY_ID_1 = "arn:aws:kms:us-east-1:999999999999:key/key1-23bv-sdfs-werw-234323nfdsf"; + private static final String KEY_ID_2 = "arn:aws:kms:us-east-1:999999999999:key/key2-02ds-wvjs-aswe-a4923489273"; + private static final AwsKmsCmkId GENERATOR_KEY = AwsKmsCmkId.fromString(GENERATOR_KEY_ID); + private static final AwsKmsCmkId KEY_1 = AwsKmsCmkId.fromString(KEY_ID_1); + private static final AwsKmsCmkId KEY_2 = AwsKmsCmkId.fromString(KEY_ID_2); + private static final EncryptedDataKey ENCRYPTED_GENERATOR_KEY = new KeyBlob(AWS_KMS_PROVIDER_ID, + GENERATOR_KEY_ID.getBytes(PROVIDER_ENCODING), generate(ALGORITHM_SUITE.getDataKeyLength())); + private static final EncryptedDataKey ENCRYPTED_KEY_1 = new KeyBlob(AWS_KMS_PROVIDER_ID, + KEY_ID_1.getBytes(PROVIDER_ENCODING), generate(ALGORITHM_SUITE.getDataKeyLength())); + private static final EncryptedDataKey ENCRYPTED_KEY_2 = new KeyBlob(AWS_KMS_PROVIDER_ID, + KEY_ID_2.getBytes(PROVIDER_ENCODING), generate(ALGORITHM_SUITE.getDataKeyLength())); + private static final KeyringTraceEntry ENCRYPTED_GENERATOR_KEY_TRACE = + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, GENERATOR_KEY_ID, ENCRYPTED_DATA_KEY, SIGNED_ENCRYPTION_CONTEXT); + private static final KeyringTraceEntry ENCRYPTED_KEY_1_TRACE = + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, KEY_ID_1, ENCRYPTED_DATA_KEY, SIGNED_ENCRYPTION_CONTEXT); + private static final KeyringTraceEntry ENCRYPTED_KEY_2_TRACE = + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, KEY_ID_2, ENCRYPTED_DATA_KEY, SIGNED_ENCRYPTION_CONTEXT); + private static final KeyringTraceEntry GENERATED_DATA_KEY_TRACE = + new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, GENERATOR_KEY_ID, GENERATED_DATA_KEY); + @Mock(lenient = true) private DataKeyEncryptionDao dataKeyEncryptionDao; + private Keyring keyring; + + @BeforeEach + void setup() { + when(dataKeyEncryptionDao.encryptDataKey(GENERATOR_KEY, PLAINTEXT_DATA_KEY, ENCRYPTION_CONTEXT)).thenReturn(ENCRYPTED_GENERATOR_KEY); + when(dataKeyEncryptionDao.encryptDataKey(KEY_1, PLAINTEXT_DATA_KEY, ENCRYPTION_CONTEXT)).thenReturn(ENCRYPTED_KEY_1); + when(dataKeyEncryptionDao.encryptDataKey(KEY_2, PLAINTEXT_DATA_KEY, ENCRYPTION_CONTEXT)).thenReturn(ENCRYPTED_KEY_2); + + when(dataKeyEncryptionDao.decryptDataKey(ENCRYPTED_GENERATOR_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenReturn(new DecryptDataKeyResult(GENERATOR_KEY_ID, PLAINTEXT_DATA_KEY)); + when(dataKeyEncryptionDao.decryptDataKey(ENCRYPTED_KEY_1, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenReturn(new DecryptDataKeyResult(KEY_ID_1, PLAINTEXT_DATA_KEY)); + when(dataKeyEncryptionDao.decryptDataKey(ENCRYPTED_KEY_2, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenReturn(new DecryptDataKeyResult(KEY_ID_2, PLAINTEXT_DATA_KEY)); + + List keyIds = new ArrayList<>(); + keyIds.add(AwsKmsCmkId.fromString(KEY_ID_1)); + keyIds.add(AwsKmsCmkId.fromString(KEY_ID_2)); + keyring = new AwsKmsKeyring(dataKeyEncryptionDao, keyIds, AwsKmsCmkId.fromString(GENERATOR_KEY_ID), false); + } + + @Test + void testMalformedArns() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(new KeyBlob(AWS_KMS_PROVIDER_ID, "arn:badArn".getBytes(PROVIDER_ENCODING), new byte[]{})); + encryptedDataKeys.add(ENCRYPTED_KEY_1); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + // Malformed Arn for a non KMS provider shouldn't fail + encryptedDataKeys.clear(); + encryptedDataKeys.add(new KeyBlob("OtherProviderId", "arn:badArn".getBytes(PROVIDER_ENCODING), new byte[]{})); + assertFalse(keyring.onDecrypt(decryptionMaterials, encryptedDataKeys).hasCleartextDataKey()); + } + + @Test + void testGeneratorKeyInKeyIds() { + assertThrows(IllegalArgumentException.class, () -> new AwsKmsKeyring(dataKeyEncryptionDao, + Collections.singletonList(AwsKmsCmkId.fromString(GENERATOR_KEY_ID)), + AwsKmsCmkId.fromString(GENERATOR_KEY_ID), false)); + } + + @Test + void testNotDiscoveryNoKeysIds() { + assertThrows(IllegalArgumentException.class, () -> new AwsKmsKeyring(dataKeyEncryptionDao, + null,null, false)); + } + + @Test + void testDiscoveryWithKeyId() { + assertThrows(IllegalArgumentException.class, () -> new AwsKmsKeyring(dataKeyEncryptionDao, + null, + AwsKmsCmkId.fromString(GENERATOR_KEY_ID), true)); + assertThrows(IllegalArgumentException.class, () -> new AwsKmsKeyring(dataKeyEncryptionDao, + Collections.singletonList(AwsKmsCmkId.fromString(GENERATOR_KEY_ID)), + null, true)); + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(3, encryptionMaterials.getEncryptedDataKeys().size()); + assertEncryptedDataKeyEquals(ENCRYPTED_KEY_1, encryptionMaterials.getEncryptedDataKeys().get(0)); + assertEncryptedDataKeyEquals(ENCRYPTED_KEY_2, encryptionMaterials.getEncryptedDataKeys().get(1)); + assertEncryptedDataKeyEquals(ENCRYPTED_GENERATOR_KEY, encryptionMaterials.getEncryptedDataKeys().get(2)); + + assertEquals(3, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_GENERATOR_KEY_TRACE)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_KEY_1_TRACE)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_KEY_2_TRACE)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(ENCRYPTED_GENERATOR_KEY); + encryptedDataKeys.add(ENCRYPTED_KEY_1); + encryptedDataKeys.add(ENCRYPTED_KEY_2); + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + KeyringTraceEntry expectedKeyringTraceEntry = new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, GENERATOR_KEY_ID, KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + assertEquals(expectedKeyringTraceEntry, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testEncryptNullDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + when(dataKeyEncryptionDao.generateDataKey(GENERATOR_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenReturn(new DataKeyEncryptionDao.GenerateDataKeyResult(PLAINTEXT_DATA_KEY, ENCRYPTED_GENERATOR_KEY)); + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(PLAINTEXT_DATA_KEY, encryptionMaterials.getCleartextDataKey()); + + assertEquals(4, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(GENERATED_DATA_KEY_TRACE)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_GENERATOR_KEY_TRACE)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_KEY_1_TRACE)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_KEY_2_TRACE)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(ENCRYPTED_GENERATOR_KEY); + encryptedDataKeys.add(ENCRYPTED_KEY_1); + encryptedDataKeys.add(ENCRYPTED_KEY_2); + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + KeyringTraceEntry expectedKeyringTraceEntry = new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, GENERATOR_KEY_ID, KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + assertEquals(expectedKeyringTraceEntry, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testEncryptNullGenerator() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + Keyring keyring = new AwsKmsKeyring(dataKeyEncryptionDao, + Collections.singletonList(AwsKmsCmkId.fromString(KEY_ID_1)), null, false); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + assertEncryptedDataKeyEquals(ENCRYPTED_KEY_1, encryptionMaterials.getEncryptedDataKeys().get(0)); + + assertEquals(PLAINTEXT_DATA_KEY, encryptionMaterials.getCleartextDataKey()); + + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().contains(ENCRYPTED_KEY_1_TRACE)); + } + + @Test + void testDiscoveryEncrypt() { + keyring = new AwsKmsKeyring(dataKeyEncryptionDao, null, null, true); + + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertFalse(encryptionMaterials.hasCleartextDataKey()); + assertEquals(0, encryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testEncryptNoGeneratorOrCleartextDataKey() { + keyring = new AwsKmsKeyring(dataKeyEncryptionDao, + Collections.singletonList(AwsKmsCmkId.fromString(KEY_ID_1)), null, false); + + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder().setAlgorithm(ALGORITHM_SUITE).build(); + assertThrows(AwsCryptoException.class, () -> keyring.onEncrypt(encryptionMaterials)); + } + + @Test + void testDecryptFirstKeyFails() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + when(dataKeyEncryptionDao.decryptDataKey(ENCRYPTED_KEY_1, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)).thenThrow(new CannotUnwrapDataKeyException()); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(ENCRYPTED_KEY_1); + encryptedDataKeys.add(ENCRYPTED_KEY_2); + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + KeyringTraceEntry expectedKeyringTraceEntry = new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, KEY_ID_2, KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + assertEquals(expectedKeyringTraceEntry, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testDecryptMismatchedDataKeyException() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + when(dataKeyEncryptionDao.decryptDataKey(ENCRYPTED_KEY_1, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)).thenThrow(new MismatchedDataKeyException()); + + assertThrows(MismatchedDataKeyException.class, () -> keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_KEY_1))); + } + + @Test + void testDecryptFirstKeyWrongProvider() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + EncryptedDataKey wrongProviderKey = new KeyBlob("OtherProvider", KEY_ID_1.getBytes(PROVIDER_ENCODING), new byte[]{}); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(wrongProviderKey); + encryptedDataKeys.add(ENCRYPTED_KEY_2); + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + KeyringTraceEntry expectedKeyringTraceEntry = new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, KEY_ID_2, KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + assertEquals(expectedKeyringTraceEntry, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testDiscoveryDecrypt() { + keyring = new AwsKmsKeyring(dataKeyEncryptionDao, null, null, true); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(ENCRYPTED_KEY_1); + encryptedDataKeys.add(ENCRYPTED_KEY_2); + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + + KeyringTraceEntry expectedKeyringTraceEntry = new KeyringTraceEntry(AWS_KMS_PROVIDER_ID, KEY_ID_1, KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + assertEquals(expectedKeyringTraceEntry, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testDecryptAlreadyDecryptedDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_GENERATOR_KEY)); + + assertEquals(PLAINTEXT_DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testDecryptNoDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, Collections.emptyList()); + + assertFalse(decryptionMaterials.hasCleartextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + private void assertEncryptedDataKeyEquals(EncryptedDataKey expected, EncryptedDataKey actual) { + assertEquals(expected.getProviderId(), actual.getProviderId()); + assertArrayEquals(expected.getProviderInformation(), actual.getProviderInformation()); + assertArrayEquals(expected.getEncryptedDataKey(), actual.getEncryptedDataKey()); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java new file mode 100644 index 000000000..e0da5d523 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class KeyringTraceTest { + + @Test + void testOrderMaintained() { + KeyringTraceEntry entry1 = new KeyringTraceEntry("ns1", "name1", KeyringTraceFlag.GENERATED_DATA_KEY); + KeyringTraceEntry entry2 = new KeyringTraceEntry("ns2", "name2", KeyringTraceFlag.DECRYPTED_DATA_KEY); + KeyringTraceEntry entry3 = new KeyringTraceEntry("ns3", "name3", KeyringTraceFlag.ENCRYPTED_DATA_KEY); + + KeyringTrace trace = KeyringTrace.EMPTY_TRACE.with(new KeyringTraceEntry(entry1.getKeyNamespace(), entry1.getKeyName(), entry1.getFlags().iterator().next())); + trace = trace.with(new KeyringTraceEntry(entry2.getKeyNamespace(), entry2.getKeyName(), entry2.getFlags().iterator().next())); + trace = trace.with(new KeyringTraceEntry(entry3.getKeyNamespace(), entry3.getKeyName(), entry3.getFlags().iterator().next())); + + assertEquals(entry1, trace.getEntries().get(0)); + assertEquals(entry2, trace.getEntries().get(1)); + assertEquals(entry3, trace.getEntries().get(2)); + } + + @Test + void testImmutable() { + KeyringTrace trace = new KeyringTrace(Collections.singletonList(new KeyringTraceEntry("namespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY))); + + assertThrows(UnsupportedOperationException.class, () -> + trace.getEntries().add(new KeyringTraceEntry("ns1", "name1", KeyringTraceFlag.GENERATED_DATA_KEY))); + } + + @Test + void testKeyringTraceEntryEquals() { + KeyringTraceEntry entry1 = new KeyringTraceEntry("namespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY); + KeyringTraceEntry entry2 = new KeyringTraceEntry(entry1.getKeyNamespace(), entry1.getKeyName(), entry1.getFlags().toArray(new KeyringTraceFlag[]{})); + KeyringTraceEntry entry3 = new KeyringTraceEntry("othernamespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY); + + assertEquals(entry1, entry1); + assertEquals(entry1, entry2); + assertNotEquals(entry2, entry3); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/MasterKeyProviderCompatibilityTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MasterKeyProviderCompatibilityTest.java new file mode 100644 index 000000000..ffe759989 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MasterKeyProviderCompatibilityTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.AwsCryptoResult; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.DecryptRequest; +import com.amazonaws.encryptionsdk.EncryptRequest; +import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.TestUtils; +import com.amazonaws.encryptionsdk.internal.RandomBytesGenerator; +import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; +import com.amazonaws.encryptionsdk.kms.AwsKmsCmkId; +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class MasterKeyProviderCompatibilityTest { + + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + private static final String KEY_NAMESPACE = "TestKeyNamespace"; + private static final String KEY_NAME = "TestKeyName"; + private static final byte[] PLAINTEXT = RandomBytesGenerator.generate(1000); + private final AwsCrypto awsCrypto = new AwsCrypto(); + + @Tag(TestUtils.TAG_INTEGRATION) + @Test + void testAwsKmsKeyringCompatibility() { + MasterKeyProvider mkp = KmsMasterKeyProvider.builder() + .withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0]).build(); + Keyring keyring = StandardKeyrings.awsKms(AwsKmsCmkId.fromString(KMSTestFixtures.TEST_KEY_IDS[0])); + + testCompatibility(keyring, mkp); + } + + @Test + void testRawAesKeyringCompatibility() { + SecretKey key = generateRandomKey(); + + JceMasterKey mkp = JceMasterKey.getInstance(key, KEY_NAMESPACE, KEY_NAME, "AES/GCM/NoPadding"); + Keyring keyring = StandardKeyrings.rawAesBuilder() + .keyNamespace(KEY_NAMESPACE) + .keyName(KEY_NAME) + .wrappingKey(key) + .build(); + + testCompatibility(keyring, mkp); + } + + @Test + void testRawRsaKeyringCompatibility() throws Exception { + final RsaPaddingScheme paddingScheme = RsaPaddingScheme.OAEP_SHA512_MGF1; + final KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA"); + kg.initialize(4096); + KeyPair keyPair = kg.generateKeyPair(); + + JceMasterKey mkp = JceMasterKey.getInstance(keyPair.getPublic(), keyPair.getPrivate(), KEY_NAMESPACE, KEY_NAME, + paddingScheme.getTransformation()); + Keyring keyring = StandardKeyrings.rawRsaBuilder() + .keyNamespace(KEY_NAMESPACE) + .keyName(KEY_NAME) + .publicKey(keyPair.getPublic()) + .privateKey(keyPair.getPrivate()) + .paddingScheme(paddingScheme) + .build(); + + testCompatibility(keyring, mkp); + } + + @Tag(TestUtils.TAG_INTEGRATION) + @Test + void testMultiKeyringCompatibility() { + SecretKey key = generateRandomKey(); + MasterKeyProvider mkp1 = KmsMasterKeyProvider.builder() + .withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0]).build(); + JceMasterKey mkp2 = JceMasterKey.getInstance(key, KEY_NAMESPACE, KEY_NAME, "AES/GCM/NoPadding"); + + MasterKeyProvider mkp = MultipleProviderFactory.buildMultiProvider(mkp1, mkp2); + + Keyring keyring1 = StandardKeyrings.awsKms(AwsKmsCmkId.fromString(KMSTestFixtures.TEST_KEY_IDS[0])); + Keyring keyring2 = StandardKeyrings.rawAesBuilder() + .keyNamespace(KEY_NAMESPACE) + .keyName(KEY_NAME) + .wrappingKey(key) + .build(); + + Keyring keyring = StandardKeyrings.multi(keyring1, keyring2); + + testCompatibility(keyring, mkp); + } + + private void testCompatibility(Keyring keyring, MasterKeyProvider masterKeyProvider) { + CryptoResult mkpResult = awsCrypto.encryptData(masterKeyProvider, PLAINTEXT, ENCRYPTION_CONTEXT); + AwsCryptoResult keyringResult = awsCrypto.decrypt(DecryptRequest.builder() + .keyring(keyring) + .ciphertext(mkpResult.getResult()).build()); + + assertArrayEquals(PLAINTEXT, keyringResult.getResult()); + + keyringResult = awsCrypto.encrypt(EncryptRequest.builder() + .keyring(keyring) + .encryptionContext(ENCRYPTION_CONTEXT) + .plaintext(PLAINTEXT).build()); + mkpResult = awsCrypto.decryptData(masterKeyProvider, keyringResult.getResult()); + + assertArrayEquals(PLAINTEXT, mkpResult.getResult()); + } + + private static SecretKey generateRandomKey() { + byte[] rawKey = new byte[16]; // 128 bits + Utils.getSecureRandom().nextBytes(rawKey); + return new SecretKeySpec(rawKey, "AES"); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java new file mode 100644 index 000000000..19c22bb99 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MultiKeyringTest { + + @Mock Keyring generatorKeyring; + @Mock Keyring keyring1; + @Mock Keyring keyring2; + @Mock EncryptionMaterials encryptionMaterials; + @Mock DecryptionMaterials decryptionMaterials; + @Mock List encryptedDataKeys; + final List childrenKeyrings = new ArrayList<>(); + + @BeforeEach + void setup() { + childrenKeyrings.add(keyring1); + childrenKeyrings.add(keyring2); + } + + @Test + void testConstructor() { + assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, null)); + assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, Collections.emptyList())); + new MultiKeyring(generatorKeyring, null); + new MultiKeyring(null, Collections.singletonList(keyring1)); + } + + @Test + void testOnEncryptWithGenerator() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + when(encryptionMaterials.hasCleartextDataKey()).thenReturn(true); + when(generatorKeyring.onEncrypt(encryptionMaterials)).thenReturn(encryptionMaterials); + when(keyring1.onEncrypt(encryptionMaterials)).thenReturn(encryptionMaterials); + when(keyring2.onEncrypt(encryptionMaterials)).thenReturn(encryptionMaterials); + + keyring.onEncrypt(encryptionMaterials); + + verify(generatorKeyring).onEncrypt(encryptionMaterials); + verify(keyring1).onEncrypt(encryptionMaterials); + verify(keyring2).onEncrypt(encryptionMaterials); + } + + @Test + void testOnEncryptWithoutGenerator() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + when(encryptionMaterials.hasCleartextDataKey()).thenReturn(true); + when(keyring1.onEncrypt(encryptionMaterials)).thenReturn(encryptionMaterials); + when(keyring2.onEncrypt(encryptionMaterials)).thenReturn(encryptionMaterials); + + keyring.onEncrypt(encryptionMaterials); + + verifyNoInteractions(generatorKeyring); + verify(keyring1).onEncrypt(encryptionMaterials); + verify(keyring2).onEncrypt(encryptionMaterials); + } + + @Test + void testOnEncryptNoPlaintextDataKey() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + when(encryptionMaterials.hasCleartextDataKey()).thenReturn(false); + + assertThrows(AwsCryptoException.class, () -> keyring.onEncrypt(encryptionMaterials)); + } + + @Test + void testOnDecryptWithPlaintextDataKey() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(true); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + verifyNoInteractions(generatorKeyring, keyring1, keyring2); + } + + @Test + void testOnDecryptWithGenerator() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true); + when(generatorKeyring.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + when(keyring1.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(keyring2); + } + + @Test + void testOnDecryptWithoutGenerator() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true); + when(keyring1.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + when(keyring2.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(keyring1, keyring2); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(generatorKeyring); + } + + @Test + void testOnDecryptFailureThenSuccess() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false).thenReturn(true); + doThrow(new IllegalStateException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + when(keyring1.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(keyring2); + } + + @Test + void testOnDecryptFailure() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false); + doThrow(new AwsCryptoException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + doThrow(new IllegalStateException()).when(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + doThrow(new IllegalArgumentException()).when(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + + AwsCryptoException exception = null; + try { + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + fail(); + } catch (AwsCryptoException e) { + exception = e; + } + + assertEquals(3, exception.getSuppressed().length); + + InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + } + + @Test + void testOnDecryptNoFailuresNoPlaintextDataKeys() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasCleartextDataKey()).thenReturn(false, false, false, false); + when(generatorKeyring.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + when(keyring1.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + when(keyring2.onDecrypt(decryptionMaterials, encryptedDataKeys)).thenReturn(decryptionMaterials); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java new file mode 100644 index 000000000..0f597a3d0 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ALGORITHM; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ENCRYPTION_CONTEXT; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAME; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAMESPACE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RawAesKeyringTest { + + private final RawAesKeyring keyring = new RawAesKeyring(KEYNAMESPACE, KEYNAME, new SecretKeySpec(generate(32), "AES")); + + @Test + void testValidToDecrypt() { + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, ArrayUtils.add(KEYNAME.getBytes(StandardCharsets.UTF_8), (byte) 5), new byte[]{}))); + //Bad namespace + assertFalse(keyring.validToDecrypt(new KeyBlob( + "WrongNamespace", KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + //Bad provider info + assertFalse(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, new byte[]{1,2,3}, new byte[]{}))); + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setCleartextDataKey(DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertTrue(Utils.arrayPrefixEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation(), keyring.keyNameBytes.length)); + assertTrue(actualEncryptedDataKey.getProviderInformation().length > keyring.keyNameBytes.length); + + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYNAME, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT)); + } + + @Test + void testEncryptDecryptGenerateDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertNotNull(encryptionMaterials.getCleartextDataKey()); + assertEquals(encryptionMaterials.getCleartextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertTrue(Utils.arrayPrefixEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation(), keyring.keyNameBytes.length)); + assertTrue(actualEncryptedDataKey.getProviderInformation().length > keyring.keyNameBytes.length); + + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.GENERATED_DATA_KEY)); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(encryptionMaterials.getCleartextDataKey(), decryptionMaterials.getCleartextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT)); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java new file mode 100644 index 000000000..3b80f7711 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RawKeyringTest { + + static final String KEYNAME = "testKeyname"; + static final String KEYNAMESPACE = "testKeynamespace"; + static final CryptoAlgorithm ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + static final SecretKey DATA_KEY = new SecretKeySpec(generate(ALGORITHM.getDataKeyLength()), ALGORITHM.getDataKeyAlgo()); + static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + private static final EncryptedDataKey ENCRYPTED_DATA_KEY = new KeyBlob("keyProviderId", new byte[]{1, 2, 3}, generate(ALGORITHM.getDataKeyLength())); + private static final EncryptedDataKey INVALID_DATA_KEY = new KeyBlob("invalidProviderId", new byte[]{1, 2, 3}, generate(ALGORITHM.getDataKeyLength())); + private static final KeyringTraceEntry ENCRYPTED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final KeyringTraceEntry DECRYPTED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.DECRYPTED_DATA_KEY); + private static final KeyringTraceEntry GENERATED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.GENERATED_DATA_KEY); + @Mock(lenient = true) private JceKeyCipher jceKeyCipher; + private Keyring keyring; + + @BeforeEach + void setup() throws Exception { + when(jceKeyCipher.encryptKey(DATA_KEY.getEncoded(), KEYNAME, KEYNAMESPACE, ENCRYPTION_CONTEXT)).thenReturn(ENCRYPTED_DATA_KEY); + when(jceKeyCipher.decryptKey(ENCRYPTED_DATA_KEY, KEYNAME, ENCRYPTION_CONTEXT)).thenReturn(DATA_KEY.getEncoded()); + + keyring = new RawKeyring(KEYNAMESPACE, KEYNAME, jceKeyCipher) { + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + return !encryptedDataKey.getProviderId().equals(INVALID_DATA_KEY.getProviderId()); + } + }; + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setCleartextDataKey(DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + assertEncryptedDataKeyEquals(ENCRYPTED_DATA_KEY, encryptionMaterials.getEncryptedDataKeys().get(0)); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(ENCRYPTED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(0)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_DATA_KEY)); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testEncryptNullDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + ArgumentCaptor dataKeyCaptor = ArgumentCaptor.forClass(byte[].class); + when(jceKeyCipher.encryptKey(dataKeyCaptor.capture(), eq(KEYNAME), eq(KEYNAMESPACE), eq(ENCRYPTION_CONTEXT))).thenReturn(ENCRYPTED_DATA_KEY); + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(encryptionMaterials.getCleartextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertArrayEquals(encryptionMaterials.getCleartextDataKey().getEncoded(), dataKeyCaptor.getValue()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + assertTrue(encryptionMaterials.hasCleartextDataKey()); + assertEncryptedDataKeyEquals(ENCRYPTED_DATA_KEY, encryptionMaterials.getEncryptedDataKeys().get(0)); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(GENERATED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(0)); + assertEquals(ENCRYPTED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(1)); + } + + @Test + void testDecryptAlreadyDecryptedDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setCleartextDataKey(DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_DATA_KEY)); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testDecryptNoValidDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.singletonList(INVALID_DATA_KEY)); + + assertFalse(decryptionMaterials.hasCleartextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testDecryptNoDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.emptyList()); + + assertFalse(decryptionMaterials.hasCleartextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + + @Test + void testDecryptMultipleKeysOneInvalid() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + final List edks = new ArrayList<>(); + edks.add(INVALID_DATA_KEY); + edks.add(ENCRYPTED_DATA_KEY); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, edks); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testDecryptMultipleKeysOneException() throws GeneralSecurityException { + final EncryptedDataKey BAD_DATA_KEY = new KeyBlob("exceptionProvider", new byte[]{1, 2, 3}, new byte[]{4, 5, 6}); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + when(jceKeyCipher.decryptKey(BAD_DATA_KEY, KEYNAME, ENCRYPTION_CONTEXT)) + .thenThrow(new GeneralSecurityException("could not decrypt key")); + + final List edks = new ArrayList<>(); + edks.add(BAD_DATA_KEY); + edks.add(ENCRYPTED_DATA_KEY); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, edks); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + private void assertEncryptedDataKeyEquals(EncryptedDataKey expected, EncryptedDataKey actual) { + assertEquals(expected.getProviderId(), actual.getProviderId()); + assertArrayEquals(expected.getProviderInformation(), actual.getProviderInformation()); + assertArrayEquals(expected.getEncryptedDataKey(), actual.getEncryptedDataKey()); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java new file mode 100644 index 000000000..59fff4da1 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; +import com.amazonaws.encryptionsdk.model.DecryptionMaterials; +import com.amazonaws.encryptionsdk.model.EncryptionMaterials; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ALGORITHM; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ENCRYPTION_CONTEXT; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAME; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAMESPACE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RawRsaKeyringTest { + + private static final RsaPaddingScheme PADDING_SCHEME = RsaPaddingScheme.PKCS1; + private static RawRsaKeyring keyring; + + @BeforeAll + static void setup() throws Exception { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + keyring = new RawRsaKeyring(KEYNAMESPACE, KEYNAME, keyPair.getPublic(), keyPair.getPrivate(), PADDING_SCHEME); + } + + @Test + void testValidToDecrypt() { + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + //Provider info has extra data + assertFalse(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, ArrayUtils.add(KEYNAME.getBytes(StandardCharsets.UTF_8), (byte)5), new byte[]{}))); + //Bad namespace + assertFalse(keyring.validToDecrypt(new KeyBlob( + "WrongNamespace", KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setCleartextDataKey(DATA_KEY) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertArrayEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation()); + + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYNAME, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(DATA_KEY, decryptionMaterials.getCleartextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + } + + @Test + void testEncryptDecryptGenerateDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + encryptionMaterials = keyring.onEncrypt(encryptionMaterials); + + assertTrue(encryptionMaterials.hasCleartextDataKey()); + assertEquals(encryptionMaterials.getCleartextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertArrayEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation()); + + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.GENERATED_DATA_KEY)); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .build(); + + decryptionMaterials = keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(encryptionMaterials.getCleartextDataKey(), decryptionMaterials.getCleartextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkIdTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkIdTest.java new file mode 100644 index 000000000..6d23b2b5c --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsCmkIdTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.encryptionsdk.exception.MalformedArnException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AwsKmsCmkIdTest { + private static final String VALID_ARN = "arn:aws:kms:us-east-1:999999999999:key/01234567-89ab-cdef-fedc-ba9876543210"; + private static final String VALID_ALIAS_ARN = "arn:aws:kms:us-east-1:999999999999:alias/MyCryptoKey"; + private static final String VALID_ALIAS = "alias/MyCryptoKey"; + private static final String VALID_RAW_KEY_ID = "01234567-89ab-cdef-fedc-ba9876543210"; + + @Test + void testFromString() { + assertThrows(MalformedArnException.class, () -> AwsKmsCmkId.fromString("arn:invalid")); + + assertTrue(AwsKmsCmkId.fromString(VALID_ARN).isArn()); + assertTrue(AwsKmsCmkId.fromString(VALID_ALIAS_ARN).isArn()); + assertFalse(AwsKmsCmkId.fromString(VALID_ALIAS).isArn()); + assertFalse(AwsKmsCmkId.fromString(VALID_RAW_KEY_ID).isArn()); + } + + @Test + void testIsKeyIdWellFormed() { + assertTrue(AwsKmsCmkId.isKeyIdWellFormed(VALID_ARN)); + assertTrue(AwsKmsCmkId.isKeyIdWellFormed(VALID_ALIAS_ARN)); + assertTrue(AwsKmsCmkId.isKeyIdWellFormed(VALID_ALIAS)); + assertTrue(AwsKmsCmkId.isKeyIdWellFormed(VALID_RAW_KEY_ID)); + assertFalse(AwsKmsCmkId.isKeyIdWellFormed("arn:invalid")); + assertFalse(AwsKmsCmkId.isKeyIdWellFormed(" ")); + assertFalse(AwsKmsCmkId.isKeyIdWellFormed(null)); + } + + @Test + void testToString() { + assertEquals(VALID_ARN, AwsKmsCmkId.fromString(VALID_ARN).toString()); + } + + @Test + void testEquals() { + assertEquals(AwsKmsCmkId.fromString(VALID_ARN), AwsKmsCmkId.fromString(VALID_ARN)); + assertNotEquals(AwsKmsCmkId.fromString(VALID_ALIAS), AwsKmsCmkId.fromString(VALID_ALIAS_ARN)); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDaoTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDaoTest.java new file mode 100644 index 000000000..ff4099e45 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsDataKeyEncryptionDaoTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.RequestClientOptions; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.MismatchedDataKeyException; +import com.amazonaws.encryptionsdk.exception.UnsupportedRegionException; +import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.model.DecryptRequest; +import com.amazonaws.services.kms.model.DecryptResult; +import com.amazonaws.services.kms.model.EncryptRequest; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; +import com.amazonaws.services.kms.model.KMSInvalidStateException; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static com.amazonaws.encryptionsdk.internal.Constants.AWS_KMS_PROVIDER_ID; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AwsKmsDataKeyEncryptionDaoTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + private static final SecretKey DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + private static final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + private static final EncryptedDataKey ENCRYPTED_DATA_KEY = new KeyBlob(AWS_KMS_PROVIDER_ID, + "arn:aws:kms:us-east-1:999999999999:key/01234567-89ab-cdef-fedc-ba9876543210".getBytes(EncryptedDataKey.PROVIDER_ENCODING), generate(ALGORITHM_SUITE.getDataKeyLength())); + + @Test + void testEncryptAndDecrypt() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + EncryptedDataKey encryptedDataKeyResult = dao.encryptDataKey( + AwsKmsCmkId.fromString(keyId), DATA_KEY, ENCRYPTION_CONTEXT); + + ArgumentCaptor er = ArgumentCaptor.forClass(EncryptRequest.class); + verify(client, times(1)).encrypt(er.capture()); + + EncryptRequest actualRequest = er.getValue(); + + assertEquals(keyId, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertArrayEquals(DATA_KEY.getEncoded(), actualRequest.getPlaintext().array()); + assertUserAgent(actualRequest); + + assertEquals(AWS_KMS_PROVIDER_ID, encryptedDataKeyResult.getProviderId()); + assertArrayEquals(keyId.getBytes(EncryptedDataKey.PROVIDER_ENCODING), encryptedDataKeyResult.getProviderInformation()); + assertNotNull(encryptedDataKeyResult.getEncryptedDataKey()); + + DataKeyEncryptionDao.DecryptDataKeyResult decryptDataKeyResult = dao.decryptDataKey(encryptedDataKeyResult, ALGORITHM_SUITE, ENCRYPTION_CONTEXT); + + ArgumentCaptor decrypt = ArgumentCaptor.forClass(DecryptRequest.class); + verify(client, times(1)).decrypt(decrypt.capture()); + + DecryptRequest actualDecryptRequest = decrypt.getValue(); + assertEquals(GRANT_TOKENS, actualDecryptRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualDecryptRequest.getEncryptionContext()); + assertArrayEquals(encryptedDataKeyResult.getEncryptedDataKey(), actualDecryptRequest.getCiphertextBlob().array()); + assertUserAgent(actualDecryptRequest); + + assertEquals(DATA_KEY, decryptDataKeyResult.getPlaintextDataKey()); + assertEquals(keyId, decryptDataKeyResult.getKeyArn()); + } + + @Test + void testGenerateAndDecrypt() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + DataKeyEncryptionDao.GenerateDataKeyResult generateDataKeyResult = dao.generateDataKey( + AwsKmsCmkId.fromString(keyId), ALGORITHM_SUITE, ENCRYPTION_CONTEXT); + + ArgumentCaptor gr = ArgumentCaptor.forClass(GenerateDataKeyRequest.class); + verify(client, times(1)).generateDataKey(gr.capture()); + + GenerateDataKeyRequest actualRequest = gr.getValue(); + + assertEquals(keyId, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), actualRequest.getNumberOfBytes()); + assertUserAgent(actualRequest); + + assertNotNull(generateDataKeyResult.getPlaintextDataKey()); + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), generateDataKeyResult.getPlaintextDataKey().getEncoded().length); + assertEquals(ALGORITHM_SUITE.getDataKeyAlgo(), generateDataKeyResult.getPlaintextDataKey().getAlgorithm()); + assertNotNull(generateDataKeyResult.getEncryptedDataKey()); + + DataKeyEncryptionDao.DecryptDataKeyResult decryptDataKeyResult = dao.decryptDataKey(generateDataKeyResult.getEncryptedDataKey(), ALGORITHM_SUITE, ENCRYPTION_CONTEXT); + + ArgumentCaptor decrypt = ArgumentCaptor.forClass(DecryptRequest.class); + verify(client, times(1)).decrypt(decrypt.capture()); + + DecryptRequest actualDecryptRequest = decrypt.getValue(); + assertEquals(GRANT_TOKENS, actualDecryptRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualDecryptRequest.getEncryptionContext()); + assertArrayEquals(generateDataKeyResult.getEncryptedDataKey().getEncryptedDataKey(), actualDecryptRequest.getCiphertextBlob().array()); + assertUserAgent(actualDecryptRequest); + + assertEquals(generateDataKeyResult.getPlaintextDataKey(), decryptDataKeyResult.getPlaintextDataKey()); + assertEquals(keyId, decryptDataKeyResult.getKeyArn()); + } + + @Test + void testEncryptWithRawKeyId() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + String rawKeyId = keyId.split("/")[1]; + EncryptedDataKey encryptedDataKeyResult = dao.encryptDataKey( + AwsKmsCmkId.fromString(rawKeyId), DATA_KEY, ENCRYPTION_CONTEXT); + + ArgumentCaptor er = ArgumentCaptor.forClass(EncryptRequest.class); + verify(client, times(1)).encrypt(er.capture()); + + EncryptRequest actualRequest = er.getValue(); + + assertEquals(rawKeyId, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertArrayEquals(DATA_KEY.getEncoded(), actualRequest.getPlaintext().array()); + assertUserAgent(actualRequest); + + assertEquals(AWS_KMS_PROVIDER_ID, encryptedDataKeyResult.getProviderId()); + assertArrayEquals(keyId.getBytes(EncryptedDataKey.PROVIDER_ENCODING), encryptedDataKeyResult.getProviderInformation()); + assertNotNull(encryptedDataKeyResult.getEncryptedDataKey()); + } + + @Test + void testEncryptWrongKeyFormat() { + SecretKey key = mock(SecretKey.class); + when(key.getFormat()).thenReturn("BadFormat"); + + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + + assertThrows(IllegalArgumentException.class, () -> dao.encryptDataKey( + AwsKmsCmkId.fromString(keyId), key, ENCRYPTION_CONTEXT)); + } + + @Test + void testKmsFailure() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + doThrow(new KMSInvalidStateException("fail")).when(client).generateDataKey(isA(GenerateDataKeyRequest.class)); + doThrow(new KMSInvalidStateException("fail")).when(client).encrypt(isA(EncryptRequest.class)); + doThrow(new KMSInvalidStateException("fail")).when(client).decrypt(isA(DecryptRequest.class)); + + assertThrows(AwsCryptoException.class, () -> dao.generateDataKey( + AwsKmsCmkId.fromString(keyId), ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + assertThrows(AwsCryptoException.class, () -> dao.encryptDataKey( + AwsKmsCmkId.fromString(keyId), DATA_KEY, ENCRYPTION_CONTEXT)); + assertThrows(AwsCryptoException.class, () -> dao.decryptDataKey(ENCRYPTED_DATA_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + + @Test + void testUnsupportedRegionException() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + String keyId = client.createKey().getKeyMetadata().getArn(); + doThrow(new UnsupportedRegionException("fail")).when(client).generateDataKey(isA(GenerateDataKeyRequest.class)); + doThrow(new UnsupportedRegionException("fail")).when(client).encrypt(isA(EncryptRequest.class)); + doThrow(new UnsupportedRegionException("fail")).when(client).decrypt(isA(DecryptRequest.class)); + + assertThrows(AwsCryptoException.class, () -> dao.generateDataKey( + AwsKmsCmkId.fromString(keyId), ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + assertThrows(AwsCryptoException.class, () -> dao.encryptDataKey( + AwsKmsCmkId.fromString(keyId), DATA_KEY, ENCRYPTION_CONTEXT)); + assertThrows(AwsCryptoException.class, () -> dao.decryptDataKey(ENCRYPTED_DATA_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + + @Test + void testDecryptBadKmsKeyId() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + DecryptResult badResult = new DecryptResult(); + badResult.setKeyId("badKeyId"); + + doReturn(badResult).when(client).decrypt(isA(DecryptRequest.class)); + + assertThrows(MismatchedDataKeyException.class, () -> dao.decryptDataKey(ENCRYPTED_DATA_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + + @Test + void testDecryptBadKmsKeyLength() { + AWSKMS client = spy(new MockKMSClient()); + DataKeyEncryptionDao dao = new AwsKmsDataKeyEncryptionDao(s -> client, GRANT_TOKENS); + + DecryptResult badResult = new DecryptResult(); + badResult.setKeyId(new String(ENCRYPTED_DATA_KEY.getProviderInformation(), EncryptedDataKey.PROVIDER_ENCODING)); + badResult.setPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength() + 1)); + + doReturn(badResult).when(client).decrypt(isA(DecryptRequest.class)); + + assertThrows(IllegalStateException.class, () -> dao.decryptDataKey(ENCRYPTED_DATA_KEY, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + + private void assertUserAgent(AmazonWebServiceRequest request) { + assertTrue(request.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) + .contains(VersionInfo.USER_AGENT)); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java index 1a58496dd..7d3062e67 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java @@ -1,10 +1,10 @@ package com.amazonaws.encryptionsdk.kms; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; @@ -19,7 +19,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; +import com.amazonaws.encryptionsdk.TestUtils; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import com.amazonaws.AbortedException; @@ -41,9 +43,10 @@ import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; -public class KMSProviderBuilderIntegrationTests { +@Tag(TestUtils.TAG_INTEGRATION) +class KMSProviderBuilderIntegrationTests { @Test - public void whenBogusRegionsDecrypted_doesNotLeakClients() throws Exception { + void whenBogusRegionsDecrypted_doesNotLeakClients() { AtomicReference> kmsCache = new AtomicReference<>(); KmsMasterKeyProvider mkp = (new KmsMasterKeyProvider.Builder() { @@ -75,7 +78,7 @@ public void whenBogusRegionsDecrypted_doesNotLeakClients() throws Exception { } @Test - public void whenOperationSuccessful_clientIsCached() { + void whenOperationSuccessful_clientIsCached() { AtomicReference> kmsCache = new AtomicReference<>(); KmsMasterKeyProvider mkp = (new KmsMasterKeyProvider.Builder() { @@ -99,7 +102,7 @@ public void whenOperationSuccessful_clientIsCached() { } @Test - public void whenConstructedWithoutArguments_canUseMultipleRegions() throws Exception { + void whenConstructedWithoutArguments_canUseMultipleRegions() { KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder().build(); for (String key : KMSTestFixtures.TEST_KEY_IDS) { @@ -115,25 +118,27 @@ public void whenConstructedWithoutArguments_canUseMultipleRegions() throws Excep } } - @SuppressWarnings("deprecation") @Test(expected = CannotUnwrapDataKeyException.class) - public void whenLegacyConstructorsUsed_multiRegionDecryptIsNotSupported() throws Exception { + @SuppressWarnings("deprecation") @Test + void whenLegacyConstructorsUsed_multiRegionDecryptIsNotSupported() { KmsMasterKeyProvider mkp = new KmsMasterKeyProvider(); - for (String key : KMSTestFixtures.TEST_KEY_IDS) { - byte[] ciphertext = - new AwsCrypto().encryptData( - KmsMasterKeyProvider.builder() - .withKeysForEncryption(key) - .build(), - new byte[1] - ).getResult(); - - new AwsCrypto().decryptData(mkp, ciphertext); - } + assertThrows(CannotUnwrapDataKeyException.class, () -> { + for (String key : KMSTestFixtures.TEST_KEY_IDS) { + byte[] ciphertext = + new AwsCrypto().encryptData( + KmsMasterKeyProvider.builder() + .withKeysForEncryption(key) + .build(), + new byte[1] + ).getResult(); + + new AwsCrypto().decryptData(mkp, ciphertext); + } + }); } @Test - public void whenHandlerConfigured_handlerIsInvoked() throws Exception { + void whenHandlerConfigured_handlerIsInvoked() { RequestHandler2 handler = spy(new RequestHandler2() {}); KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder() @@ -150,7 +155,7 @@ public void whenHandlerConfigured_handlerIsInvoked() throws Exception { } @Test - public void whenShortTimeoutSet_timesOut() throws Exception { + void whenShortTimeoutSet_timesOut() { // By setting a timeout of 1ms, it's not physically possible to complete both the us-west-2 and eu-central-1 // requests due to speed of light limits. KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder() @@ -179,7 +184,7 @@ public void whenShortTimeoutSet_timesOut() throws Exception { } @Test - public void whenCustomCredentialsSet_theyAreUsed() throws Exception { + void whenCustomCredentialsSet_theyAreUsed() { AWSCredentialsProvider customProvider = spy(new DefaultAWSCredentialsProviderChain()); KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder() @@ -204,7 +209,7 @@ public void whenCustomCredentialsSet_theyAreUsed() throws Exception { } @Test - public void whenBuilderCloned_credentialsAndConfigurationAreRetained() throws Exception { + void whenBuilderCloned_credentialsAndConfigurationAreRetained() { AWSCredentialsProvider customProvider1 = spy(new DefaultAWSCredentialsProviderChain()); AWSCredentialsProvider customProvider2 = spy(new DefaultAWSCredentialsProviderChain()); @@ -237,7 +242,7 @@ public void whenBuilderCloned_credentialsAndConfigurationAreRetained() throws Ex } @Test - public void whenBuilderCloned_clientBuilderCustomizationIsRetained() throws Exception { + void whenBuilderCloned_clientBuilderCustomizationIsRetained() { RequestHandler2 handler = spy(new RequestHandler2() {}); KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder() @@ -252,9 +257,9 @@ public void whenBuilderCloned_clientBuilderCustomizationIsRetained() throws Exce verify(handler, atLeastOnce()).beforeRequest(any()); } - @Test(expected = IllegalArgumentException.class) - public void whenBogusEndpointIsSet_constructionFails() throws Exception { - KmsMasterKeyProvider.builder() + @Test + void whenBogusEndpointIsSet_constructionFails() { + assertThrows(IllegalArgumentException.class, () -> KmsMasterKeyProvider.builder() .withClientBuilder( AWSKMSClientBuilder.standard() .withEndpointConfiguration( @@ -262,11 +267,11 @@ public void whenBogusEndpointIsSet_constructionFails() throws Exception { "https://this.does.not.exist.example.com", "bad-region") ) - ); + )); } @Test - public void whenUserAgentsOverridden_originalUAsPreserved() throws Exception { + void whenUserAgentsOverridden_originalUAsPreserved() { RequestHandler2 handler = spy(new RequestHandler2() {}); KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder() @@ -294,7 +299,7 @@ public void whenUserAgentsOverridden_originalUAsPreserved() throws Exception { } @Test - public void whenDefaultRegionSet_itIsUsedForBareKeyIds() throws Exception { + void whenDefaultRegionSet_itIsUsedForBareKeyIds() { // TODO: Need to set up a role to assume as bare key IDs are relative to the caller account } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java new file mode 100644 index 000000000..d1ea1a0ec --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.MismatchedDataKeyException; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static com.amazonaws.encryptionsdk.internal.Constants.AWS_KMS_PROVIDER_ID; +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class KmsMasterKeyTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("test", "value"); + private static final String CMK_ARN = "arn:aws:kms:us-east-1:999999999999:key/01234567-89ab-cdef-fedc-ba9876543210"; + @Mock AwsKmsDataKeyEncryptionDao dataKeyEncryptionDao; + + /** + * Test that when decryption of an encrypted data key throws a MismatchedDataKeyException, this + * key is skipped and another key in the list of keys is decrypted. + */ + @Test + void testMismatchedDataKeyException() { + EncryptedDataKey encryptedDataKey1 = new KeyBlob(AWS_KMS_PROVIDER_ID, "KeyId1".getBytes(PROVIDER_ENCODING), generate(64)); + EncryptedDataKey encryptedDataKey2 = new KeyBlob(AWS_KMS_PROVIDER_ID, "KeyId2".getBytes(PROVIDER_ENCODING), generate(64)); + SecretKey secretKey = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + + when(dataKeyEncryptionDao.decryptDataKey(encryptedDataKey1, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenThrow(new MismatchedDataKeyException()); + when(dataKeyEncryptionDao.decryptDataKey(encryptedDataKey2, ALGORITHM_SUITE, ENCRYPTION_CONTEXT)) + .thenReturn(new DataKeyEncryptionDao.DecryptDataKeyResult("KeyId2", secretKey)); + + KmsMasterKey kmsMasterKey = new KmsMasterKey(dataKeyEncryptionDao, CMK_ARN, null); + + List encryptedDataKeys = new ArrayList<>(); + encryptedDataKeys.add(encryptedDataKey1); + encryptedDataKeys.add(encryptedDataKey2); + + DataKey result = kmsMasterKey.decryptDataKey(ALGORITHM_SUITE, encryptedDataKeys, ENCRYPTION_CONTEXT); + + assertEquals(secretKey, result.getKey()); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/MockKMSClient.java b/src/test/java/com/amazonaws/encryptionsdk/kms/MockKMSClient.java index 37fe9cbff..00ce5c074 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/MockKMSClient.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/MockKMSClient.java @@ -29,7 +29,7 @@ import com.amazonaws.ResponseMetadata; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; -import com.amazonaws.services.kms.AWSKMSClient; +import com.amazonaws.services.kms.AbstractAWSKMS; import com.amazonaws.services.kms.model.CreateAliasRequest; import com.amazonaws.services.kms.model.CreateAliasResult; import com.amazonaws.services.kms.model.CreateGrantRequest; @@ -85,7 +85,7 @@ import com.amazonaws.services.kms.model.UpdateKeyDescriptionRequest; import com.amazonaws.services.kms.model.UpdateKeyDescriptionResult; -public class MockKMSClient extends AWSKMSClient { +public class MockKMSClient extends AbstractAWSKMS { private static final SecureRandom rnd = new SecureRandom(); private static final String ACCOUNT_ID = "01234567890"; private final Map results_ = new HashMap<>(); diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliersTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliersTest.java new file mode 100644 index 000000000..dec01d5a3 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/StandardAwsKmsClientSuppliersTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.encryptionsdk.exception.UnsupportedRegionException; +import com.amazonaws.encryptionsdk.kms.StandardAwsKmsClientSuppliers.DefaultAwsKmsClientSupplierBuilder; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClientBuilder; +import com.amazonaws.services.kms.model.AWSKMSException; +import com.amazonaws.services.kms.model.DecryptRequest; +import com.amazonaws.services.kms.model.EncryptRequest; +import com.amazonaws.services.kms.model.EncryptResult; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; +import com.amazonaws.services.kms.model.KMSInvalidStateException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StandardAwsKmsClientSuppliersTest { + + @Mock AWSKMSClientBuilder kmsClientBuilder; + @Mock AWSKMS awskms; + @Mock EncryptRequest encryptRequest; + @Mock DecryptRequest decryptRequest; + @Mock GenerateDataKeyRequest generateDataKeyRequest; + @Mock AWSCredentialsProvider credentialsProvider; + @Mock ClientConfiguration clientConfiguration; + private static final String REGION_1 = "us-east-1"; + private static final String REGION_2 = "us-west-2"; + private static final String REGION_3 = "eu-west-1"; + + @Test + void testCredentialsAndClientConfiguration() { + when(kmsClientBuilder.withClientConfiguration(clientConfiguration)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.withCredentials(credentialsProvider)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + AwsKmsClientSupplier supplier = new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder) + .credentialsProvider(credentialsProvider) + .clientConfiguration(clientConfiguration) + .build(); + + supplier.getClient(null); + + verify(kmsClientBuilder).withClientConfiguration(clientConfiguration); + verify(kmsClientBuilder).withCredentials(credentialsProvider); + verify(kmsClientBuilder).build(); + } + + @Test + void testClientCaching() { + AwsKmsClientSupplier supplier = new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder) + .build(); + + when(kmsClientBuilder.withRegion(REGION_1)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.withRegion(REGION_2)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.withRegion(REGION_3)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + AWSKMS client = supplier.getClient(REGION_1); + AWSKMS client2 = supplier.getClient(REGION_2); + AWSKMS client3 = supplier.getClient(REGION_3); + verify(kmsClientBuilder, times(3)).build(); + + // No KMS methods have been called yet, so clients remain uncached + supplier.getClient(REGION_1); + supplier.getClient(REGION_2); + supplier.getClient(REGION_3); + verify(kmsClientBuilder, times(6)).build(); + + when(awskms.encrypt(encryptRequest)).thenReturn(new EncryptResult()); + when(awskms.decrypt(decryptRequest)).thenThrow(new KMSInvalidStateException("test")); + when(awskms.generateDataKey(generateDataKeyRequest)).thenThrow(new IllegalArgumentException("test")); + + // Successful KMS call, client is cached + client.encrypt(encryptRequest); + supplier.getClient(REGION_1); + verify(kmsClientBuilder, times(6)).build(); + + // KMS call resulted in KMS exception, client is cached + assertThrows(AWSKMSException.class, () -> client2.decrypt(decryptRequest)); + supplier.getClient(REGION_2); + verify(kmsClientBuilder, times(6)).build(); + + // KMS call resulted in non-KMS exception, client is not cached + assertThrows(IllegalArgumentException.class, () -> client3.generateDataKey(generateDataKeyRequest)); + supplier.getClient(REGION_3); + verify(kmsClientBuilder, times(7)).build(); + + // Non-KMS method, client is not cached + client3.toString(); + supplier.getClient(REGION_3); + verify(kmsClientBuilder, times(8)).build(); + } + + @Test + void testGetClientByKeyId() { + + final String arn = "arn:aws:kms:us-east-1:999999999999:key/01234567-89ab-cdef-fedc-ba9876543210"; + final String aliasArn = "arn:aws:kms:us-east-1:999999999999:alias/MyCryptoKey"; + final String alias = "alias/MyCryptoKey"; + final String keyId = "01234567-89ab-cdef-fedc-ba9876543210"; + + assertEquals(awskms, AwsKmsClientSupplier.getClientByKeyId(AwsKmsCmkId.fromString(arn), s -> awskms)); + assertEquals(awskms, AwsKmsClientSupplier.getClientByKeyId(AwsKmsCmkId.fromString(aliasArn), s -> awskms)); + assertEquals(awskms, AwsKmsClientSupplier.getClientByKeyId(AwsKmsCmkId.fromString(alias), s -> awskms)); + assertEquals(awskms, AwsKmsClientSupplier.getClientByKeyId(AwsKmsCmkId.fromString(keyId), s -> awskms)); + } + + @Test + void testAllowedRegions() { + AwsKmsClientSupplier supplierWithDefaultValues = new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder) + .build(); + + when(kmsClientBuilder.withRegion(REGION_1)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + assertNotNull(supplierWithDefaultValues.getClient(REGION_1)); + + AwsKmsClientSupplier supplierWithAllowed = StandardAwsKmsClientSuppliers + .allowRegionsBuilder(Collections.singleton(REGION_1)) + .baseClientSupplier(new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder).build()).build(); + + when(kmsClientBuilder.withRegion(REGION_1)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + assertNotNull(supplierWithAllowed.getClient(REGION_1)); + assertThrows(UnsupportedRegionException.class, () -> supplierWithAllowed.getClient(REGION_2)); + } + + @Test + void testDeniedRegions() { + AwsKmsClientSupplier supplierWithDefaultValues = new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder) + .build(); + + when(kmsClientBuilder.withRegion(REGION_1)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + assertNotNull(supplierWithDefaultValues.getClient(REGION_1)); + + AwsKmsClientSupplier supplierWithDenied = StandardAwsKmsClientSuppliers + .denyRegionsBuilder(Collections.singleton(REGION_1)) + .baseClientSupplier(new DefaultAwsKmsClientSupplierBuilder(kmsClientBuilder).build()).build(); + + when(kmsClientBuilder.withRegion(REGION_2)).thenReturn(kmsClientBuilder); + when(kmsClientBuilder.build()).thenReturn(awskms); + + assertThrows(UnsupportedRegionException.class, () -> supplierWithDenied.getClient(REGION_1)); + assertNotNull(supplierWithDenied.getClient(REGION_2)); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/XCompatKmsDecryptTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/XCompatKmsDecryptTest.java deleted file mode 100644 index f7e7c40f2..000000000 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/XCompatKmsDecryptTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except - * in compliance with the License. A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package com.amazonaws.encryptionsdk.kms; - -import static org.junit.Assert.assertArrayEquals; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import com.amazonaws.encryptionsdk.AwsCrypto; -import com.amazonaws.encryptionsdk.CryptoResult; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -@RunWith(Parameterized.class) -public class XCompatKmsDecryptTest { - private String plaintextFileName; - private String ciphertextFileName; - private String kmsKeyId; - - public XCompatKmsDecryptTest(String plaintextFileName, String ciphertextFileName, String kmsKeyId) { - this.plaintextFileName = plaintextFileName; - this.ciphertextFileName = ciphertextFileName; - this.kmsKeyId = kmsKeyId; - } - - @Parameters(name="{index}: testDecryptFromFile({0}, {1}, {2})") - public static Collection data() throws Exception { - String baseDirName; - baseDirName = System.getProperty("staticCompatibilityResourcesDir"); - if (baseDirName == null) { - baseDirName = - XCompatKmsDecryptTest.class.getProtectionDomain().getCodeSource().getLocation().getPath() + - "aws_encryption_sdk_resources"; - } - - List testCases_ = new ArrayList(); - - String ciphertextManifestName = StringUtils.join( - new String[]{ - baseDirName, - "manifests", - "ciphertext.manifest" - }, - File.separator - ); - File ciphertextManifestFile = new File(ciphertextManifestName); - - if (!ciphertextManifestFile.exists()) { - return Collections.emptyList(); - } - - ObjectMapper ciphertextManifestMapper = new ObjectMapper(); - Map ciphertextManifest = ciphertextManifestMapper.readValue( - ciphertextManifestFile, - new TypeReference>(){} - ); - - List> testCases = (List>)ciphertextManifest.get("test_cases"); - for (Map testCase : testCases) { - Map plaintext = (Map)testCase.get("plaintext"); - Map ciphertext = (Map)testCase.get("ciphertext"); - - List> masterKeys = (List>)testCase.get("master_keys"); - for (Map masterKey : masterKeys) { - String providerId = (String) masterKey.get("provider_id"); - if (providerId.equals("aws-kms") && (boolean)masterKey.get("decryptable")) { - testCases_.add(new Object[] { - baseDirName + File.separator + plaintext.get("filename"), - baseDirName + File.separator + ciphertext.get("filename"), - (String)masterKey.get("key_id") - }); - break; - } - } - } - return testCases_; - } - - @Test - public void testDecryptFromFile() throws Exception { - AwsCrypto crypto = new AwsCrypto(); - final KmsMasterKeyProvider masterKeyProvider = new KmsMasterKeyProvider(kmsKeyId); - byte ciphertextBytes[] = Files.readAllBytes(Paths.get(ciphertextFileName)); - byte plaintextBytes[] = Files.readAllBytes(Paths.get(plaintextFileName)); - final CryptoResult decryptResult = crypto.decryptData( - masterKeyProvider, - ciphertextBytes - ); - assertArrayEquals(plaintextBytes, (byte[])decryptResult.getResult()); - } -} \ No newline at end of file diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java new file mode 100644 index 000000000..ea5f67e26 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/model/DecryptionMaterialsTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.model; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceEntry; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.Collections; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DecryptionMaterialsTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("testKey", "testValue"); + private static final KeyringTraceEntry KEYRING_TRACE_ENTRY = new KeyringTraceEntry("Namespace", "Name", KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final KeyringTrace KEYRING_TRACE = new KeyringTrace(Collections.singletonList(KEYRING_TRACE_ENTRY)); + private static final SecretKey PLAINTEXT_DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + private static PublicKey VERIFICATION_KEY; + + @BeforeAll + static void setup() throws Exception { + + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = TrailingSignatureAlgorithm.forCryptoAlgorithm(ALGORITHM_SUITE).generateKey(); + VERIFICATION_KEY = keyPair.getPublic(); + } + + @Test + void testBuilder() { + DecryptionMaterials result = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .setKeyringTrace(KEYRING_TRACE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setTrailingSignatureKey(VERIFICATION_KEY) + .build(); + + assertEquals(ALGORITHM_SUITE, result.getAlgorithm()); + assertEquals(ENCRYPTION_CONTEXT, result.getEncryptionContext()); + assertEquals(KEYRING_TRACE, result.getKeyringTrace()); + assertEquals(PLAINTEXT_DATA_KEY, result.getCleartextDataKey()); + assertEquals(VERIFICATION_KEY, result.getTrailingSignatureKey()); + } + + @Test + void testInvalidPlaintextDataKey() { + SecretKey wrongLength = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength() + 1), ALGORITHM_SUITE.getDataKeyAlgo()); + SecretKey wrongAlgorithm = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), "InvalidAlgorithm"); + + + DecryptionMaterials materials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setTrailingSignatureKey(VERIFICATION_KEY) + .build(); + assertThrows(IllegalArgumentException.class, () -> materials + .withCleartextDataKey(wrongAlgorithm, KEYRING_TRACE_ENTRY)); + assertThrows(IllegalArgumentException.class, () -> materials + .withCleartextDataKey(wrongLength, KEYRING_TRACE_ENTRY)); + } + + @Test + void testToBuilder() { + DecryptionMaterials expected = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .setKeyringTrace(KEYRING_TRACE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setTrailingSignatureKey(VERIFICATION_KEY) + .build(); + + DecryptionMaterials actual = expected.toBuilder().build(); + + assertEquals(expected, actual); + assertNotSame(expected, actual); + } + + @Test + void testWithPlaintextDataKey() { + final DecryptionMaterials materials = DecryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setTrailingSignatureKey(VERIFICATION_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.withCleartextDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.withCleartextDataKey(PLAINTEXT_DATA_KEY, null)); + + final DecryptionMaterials newMaterials = materials.withCleartextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(PLAINTEXT_DATA_KEY, newMaterials.getCleartextDataKey()); + assertEquals(PLAINTEXT_DATA_KEY, newMaterials.getDataKey().getKey()); + assertEquals(1, newMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, newMaterials.getKeyringTrace().getEntries().get(0)); + + assertThrows(IllegalStateException.class, () -> newMaterials.withCleartextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY)); + } + + @Test + void testGetOptionalProperties() { + DecryptionMaterials materials = DecryptionMaterials.newBuilder() + .build(); + + assertNull(materials.getAlgorithm()); + assertNull(materials.getCleartextDataKey()); + assertFalse(materials.hasCleartextDataKey()); + assertNull(materials.getTrailingSignatureKey()); + assertTrue(materials.getEncryptionContext().isEmpty()); + assertTrue(materials.getKeyringTrace().getEntries().isEmpty()); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/EncryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/EncryptionMaterialsTest.java new file mode 100644 index 000000000..ee06a089c --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/model/EncryptionMaterialsTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.model; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceEntry; +import com.amazonaws.encryptionsdk.keyrings.KeyringTraceFlag; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.util.Collections; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +class EncryptionMaterialsTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("testKey", "testValue"); + private static final KeyringTraceEntry KEYRING_TRACE_ENTRY = new KeyringTraceEntry("Namespace", "Name", KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final KeyringTrace KEYRING_TRACE = new KeyringTrace(Collections.singletonList(KEYRING_TRACE_ENTRY)); + private static final SecretKey PLAINTEXT_DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + @Mock + private static KeyBlob ENCRYPTED_DATA_KEY; + private static PrivateKey SIGNING_KEY; + + @BeforeAll + static void setup() throws Exception { + + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = TrailingSignatureAlgorithm.forCryptoAlgorithm(ALGORITHM_SUITE).generateKey(); + SIGNING_KEY = keyPair.getPrivate(); + } + + @Test + void testBuilder() { + EncryptionMaterials result = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .setKeyringTrace(KEYRING_TRACE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setEncryptedDataKeys(Collections.singletonList(ENCRYPTED_DATA_KEY)) + .setTrailingSignatureKey(SIGNING_KEY) + .build(); + + assertEquals(ALGORITHM_SUITE, result.getAlgorithm()); + assertEquals(ENCRYPTION_CONTEXT, result.getEncryptionContext()); + assertEquals(KEYRING_TRACE, result.getKeyringTrace()); + assertEquals(PLAINTEXT_DATA_KEY, result.getCleartextDataKey()); + assertEquals(1, result.getEncryptedDataKeys().size()); + assertEquals(ENCRYPTED_DATA_KEY, result.getEncryptedDataKeys().get(0)); + assertEquals(SIGNING_KEY, result.getTrailingSignatureKey()); + } + + @Test + void testInvalidPlaintextDataKey() { + SecretKey wrongLength = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength() + 1), ALGORITHM_SUITE.getDataKeyAlgo()); + SecretKey wrongAlgorithm = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), "InvalidAlgorithm"); + + EncryptionMaterials materials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setTrailingSignatureKey(SIGNING_KEY) + .build(); + assertThrows(IllegalArgumentException.class, () -> materials + .withCleartextDataKey(wrongAlgorithm, KEYRING_TRACE_ENTRY)); + assertThrows(IllegalArgumentException.class, () -> materials + .withCleartextDataKey(wrongLength, KEYRING_TRACE_ENTRY)); + } + + @Test + void testToBuilder() { + EncryptionMaterials expected = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setEncryptionContext(ENCRYPTION_CONTEXT) + .setKeyringTrace(KEYRING_TRACE) + .setCleartextDataKey(PLAINTEXT_DATA_KEY) + .setEncryptedDataKeys(Collections.singletonList(ENCRYPTED_DATA_KEY)) + .setTrailingSignatureKey(SIGNING_KEY) + .build(); + + EncryptionMaterials actual = expected.toBuilder().build(); + + assertEquals(expected, actual); + assertNotSame(expected, actual); + } + + @Test + void testWithEncryptedDataKey() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setTrailingSignatureKey(SIGNING_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.withEncryptedDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.withEncryptedDataKey(ENCRYPTED_DATA_KEY, null)); + + EncryptionMaterials newMaterials = materials.withEncryptedDataKey(ENCRYPTED_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(1, newMaterials.getEncryptedDataKeys().size()); + assertEquals(ENCRYPTED_DATA_KEY, newMaterials.getEncryptedDataKeys().get(0)); + assertEquals(1, newMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, newMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testWithPlaintextDataKey() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder() + .setAlgorithm(ALGORITHM_SUITE) + .setTrailingSignatureKey(SIGNING_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.withCleartextDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.withCleartextDataKey(PLAINTEXT_DATA_KEY, null)); + + EncryptionMaterials newMaterials = materials.withCleartextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(PLAINTEXT_DATA_KEY, newMaterials.getCleartextDataKey()); + assertEquals(1, newMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, newMaterials.getKeyringTrace().getEntries().get(0)); + + assertThrows(IllegalStateException.class, () -> newMaterials.withCleartextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY)); + } + + @Test + void testGetOptionalProperties() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder() + .build(); + + assertNull(materials.getAlgorithm()); + assertNull(materials.getCleartextDataKey()); + assertFalse(materials.hasCleartextDataKey()); + assertTrue(materials.getEncryptedDataKeys().isEmpty()); + assertNull(materials.getTrailingSignatureKey()); + assertTrue(materials.getKeyringTrace().getEntries().isEmpty()); + assertTrue(materials.getEncryptionContext().isEmpty()); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/RsaPaddingSchemeTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/RsaPaddingSchemeTest.java new file mode 100644 index 000000000..30a96988c --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/model/RsaPaddingSchemeTest.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.model; + +import com.amazonaws.encryptionsdk.keyrings.RawRsaKeyringBuilder.RsaPaddingScheme; +import org.junit.jupiter.api.Test; + +import javax.crypto.Cipher; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RsaPaddingSchemeTest { + + @Test + void testCipherInitialization() throws Exception { + for (RsaPaddingScheme paddingScheme : RsaPaddingScheme.values()) { + assertNotNull(Cipher.getInstance(paddingScheme.getTransformation())); + } + } +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..d41cd3cca --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +# Enables mocking final types +mock-maker-inline