diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ccfc619b..1458c3127 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,27 @@
# Changelog
-## 1.6.1 -- Unreleased
+## 1.6.1 -- 2019-10-29
+
+### Deprecation Warnings
+* Deprecated `AwsCrypto.encryptString()` and `AwsCrypto.decryptString()`.
+ Replace your calls to these methods with calls to AwsCrypto.encryptData() and AwsCrypto.decryptData().
+ Unlike the deprecated methods, these methods don't perform any Base64 encoding or decoding, so they are fully compatible with other language implementations of the AWS Encryption SDK.
+
+ If you need Base64 encoding or decoding for your application, you can add it outside of the AWS Encryption SDK.
+ [PR #120](https://github.com/aws/aws-encryption-sdk-java/pull/120)
+
+### Patches
+* Correctly validate version [PR #116](https://github.com/aws/aws-encryption-sdk-java/pull/116)
+* `ParsedCiphertext` now handles truncated input properly [PR #119](https://github.com/aws/aws-encryption-sdk-java/pull/119)
+
### Maintenance
-* Add support for standard test vectors via `testVectorZip` system property.
-* No longer require use of BouncyCastle with RSA `JceMasterKey`s
-* No longer use BouncyCastle for Elliptic Curve key generation and point compression/decompression
+* Add support for standard test vectors via `testVectorZip` system property. [PR #127](https://github.com/aws/aws-encryption-sdk-java/pull/127)
+* Remove all explicit cryptographic dependencies on BouncyCastle. The AWS Encryption SDK for Java still uses Bouncy Castle for other tasks. PRs
+ [#128](https://github.com/aws/aws-encryption-sdk-java/pull/128),
+ [#129](https://github.com/aws/aws-encryption-sdk-java/pull/129),
+ [#130](https://github.com/aws/aws-encryption-sdk-java/pull/130),
+ [#131](https://github.com/aws/aws-encryption-sdk-java/pull/131),
+ and [#132](https://github.com/aws/aws-encryption-sdk-java/pull/132).
## 1.6.0 -- 2019-05-31
@@ -17,7 +34,7 @@
## 1.5.0 -- 2019-05-30
### Minor Changes
-* Add dependency on Apache Commons Codec 1.12.
+* Added dependency on Apache Commons Codec 1.12.
* Use org.apache.commons.codec.binary.Base64 instead of java.util.Base64 so
that the SDK can be used on systems that do not have java.util.Base64 but
support Java 8 language features.
diff --git a/README.md b/README.md
index c8ad880d0..0387070cc 100644
--- a/README.md
+++ b/README.md
@@ -9,26 +9,31 @@ For more details about the design and architecture of the SDK, see the [official
### Required Prerequisites
To use this SDK you must have:
-* **A Java 8 development environment**
+* **A Java 8 or newer development environment**
- If you do not have one, go to [Java SE Downloads](https://www.oracle.com/technetwork/java/javase/downloads/index.html) on the Oracle website, then download and install the Java SE Development Kit (JDK). Java 8 or higher is required.
+ If you do not have one, we recommend [Amazon Corretto](https://aws.amazon.com/corretto/).
**Note:** If you use the Oracle JDK, you must also download and install the [Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html).
-* **Bouncy Castle**
+* **Bouncy Castle** or **Bouncy Castle FIPS**
- Bouncy Castle provides a cryptography API for Java. If you do not have Bouncy Castle, go to https://bouncycastle.org/latest_releases.html, then download the provider file that corresponds to your JDK. Or, you can pick it up from Maven:
+ The AWS Encryption SDK for Java uses Bouncy Castle to serialize and deserialize cryptographic objects.
+ It does not explicitly use Bouncy Castle (or any other [JCA Provider](https://docs.oracle.com/javase/8/docs/api/java/security/Provider.html)) for the underlying cryptography.
+ Instead, it uses the platform default, which you can configure or override as documented in the
+ [Java Cryptography Architecture (JCA) Reference Guide](https://docs.oracle.com/javase/9/security/java-cryptography-architecture-jca-reference-guide.htm#JSSEC-GUID-2BCFDD85-D533-4E6C-8CE9-29990DEB0190).
- ```xml
-
- org.bouncycastle
- bcprov-ext-jdk15on
- 1.61
-
- ```
+ If you do not have Bouncy Castle, go to https://bouncycastle.org/latest_releases.html, then download the provider file that corresponds to your JDK.
+ Or, you can pick it up from Maven (groupId: `org.bouncycastle`, artifactId: `bcprov-ext-jdk15on`).
+
+ Beginning in version 1.6.1,
+ the AWS Encryption SDK also works with Bouncy Castle FIPS (groupId: `org.bouncycastle`, artifactId: `bc-fips`)
+ as an alternative to non-FIPS Bouncy Castle.
+ For help installing and configuring Bouncy Castle FIPS properly, see [BC FIPS documentation](https://www.bouncycastle.org/documentation.html),
+ in particular, **User Guides** and **Security Policy**.
### Optional Prerequisites
+#### AWS Integration
You don't need an Amazon Web Services (AWS) account to use this SDK, but some of the [example code][examples] requires an AWS account, a customer master key (CMK) in AWS KMS, and the AWS SDK for Java.
* **To create an AWS account**, go to [Sign In or Create an AWS Account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) and then choose **I am a new user.** Follow the instructions to create an AWS account.
@@ -37,6 +42,10 @@ You don't need an Amazon Web Services (AWS) account to use this SDK, but some of
* **To download and install the AWS SDK for Java**, go to [Installing the AWS SDK for Java](https://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-install-sdk.html) in the AWS SDK for Java documentation and then follow the instructions on that page.
+#### Amazon Corretto Crypto Provider
+Many users find that the Amazon Corretto Crypto Provider (ACCP) significantly improves the performance of the AWS Encryption SDK.
+For help installing and using ACCP, see the [ACCP GitHub Respository](https://github.com/corretto/amazon-corretto-crypto-provider) .
+
### Download
You can get the latest release from Maven:
@@ -45,29 +54,10 @@ You can get the latest release from Maven:
com.amazonawsaws-encryption-sdk-java
- 1.6.0
+ 1.6.1
```
-Don't forget to enable the download of snapshot jars from Maven:
-
-```xml
-
-
- allow-snapshots
- true
-
-
- snapshots-repo
- https://oss.sonatype.org/content/repositories/snapshots
- false
- true
-
-
-
-
-```
-
### Get Started
The following code sample demonstrates how to get started:
diff --git a/pom.xml b/pom.xml
index 3a2f2d32c..6c485750c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.amazonawsaws-encryption-sdk-java
- 1.6.0
+ 1.6.1jaraws-encryption-sdk-java
@@ -118,6 +118,26 @@
8
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-test-source
+ generate-test-sources
+
+ add-test-source
+
+
+
+ src/examples/java
+
+
+
+
+
@@ -145,6 +165,12 @@
sign
+
+
+ --pinentry-mode
+ loopback
+
+
diff --git a/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java b/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java
new file mode 100644
index 000000000..748cea536
--- /dev/null
+++ b/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java
@@ -0,0 +1,88 @@
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+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;
+
+/**
+ *
+ * Encrypts and then decrypts data using an AWS KMS customer master key.
+ *
+ *
+ * Arguments:
+ *
+ *
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
+ *
+ */
+public class BasicEncryptionExample {
+
+ private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8);
+
+ public static void main(final String[] args) {
+ final String keyArn = args[0];
+
+ encryptAndDecrypt(keyArn);
+ }
+
+ static void encryptAndDecrypt(final String keyArn) {
+ // 1. Instantiate the SDK
+ final AwsCrypto crypto = new AwsCrypto();
+
+ // 2. Instantiate a KMS master key provider
+ final KmsMasterKeyProvider masterKeyProvider = KmsMasterKeyProvider.builder().withKeysForEncryption(keyArn).build();
+
+ // 3. Create an encryption context
+ //
+ // Most encrypted data should have an associated encryption context
+ // to protect integrity. This sample uses placeholder values.
+ //
+ // For more information see:
+ // 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
+ final CryptoResult encryptResult = crypto.encryptData(masterKeyProvider, EXAMPLE_DATA, encryptionContext);
+ final byte[] ciphertext = encryptResult.getResult();
+
+ // 5. Decrypt the data
+ final CryptoResult decryptResult = crypto.decryptData(masterKeyProvider, ciphertext);
+
+ // 6. Before verifying the plaintext, verify that the customer master key that
+ // was used in the encryption operation was the one supplied to the master key provider.
+ if (!decryptResult.getMasterKeyIds().get(0).equals(keyArn)) {
+ throw new IllegalStateException("Wrong key ID!");
+ }
+
+ // 7. Also, verify that the encryption context in the result contains the
+ // encryption context supplied to the encryptData method. Because the
+ // SDK can add values to the encryption context, don't require that
+ // the entire context matches.
+ if (!encryptionContext.entrySet().stream()
+ .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) {
+ throw new IllegalStateException("Wrong Encryption Context!");
+ }
+
+ // 8. Verify that the decrypted plaintext matches the original plaintext
+ assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA);
+ }
+}
diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java
new file mode 100644
index 000000000..7a4511f01
--- /dev/null
+++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java
@@ -0,0 +1,94 @@
+/*
+ * 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.internal;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.util.Map;
+
+/**
+ * A JceKeyCipher based on the Advanced Encryption Standard in Galois/Counter Mode.
+ */
+class AesGcmJceKeyCipher extends JceKeyCipher {
+ private static final int NONCE_LENGTH = 12;
+ private static final int TAG_LENGTH = 128;
+ private static final String TRANSFORMATION = "AES/GCM/NoPadding";
+ private static final int SPEC_LENGTH = Integer.BYTES + Integer.BYTES + NONCE_LENGTH;
+
+ AesGcmJceKeyCipher(SecretKey key) {
+ super(key, key);
+ }
+
+ private static byte[] specToBytes(final GCMParameterSpec spec) {
+ final byte[] nonce = spec.getIV();
+ final byte[] result = new byte[SPEC_LENGTH];
+ final ByteBuffer buffer = ByteBuffer.wrap(result);
+ buffer.putInt(spec.getTLen());
+ buffer.putInt(nonce.length);
+ buffer.put(nonce);
+ return result;
+ }
+
+ private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws InvalidKeyException {
+ if (data.length - offset != SPEC_LENGTH) {
+ throw new InvalidKeyException("Algorithm specification was an invalid data size");
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(data, offset, SPEC_LENGTH);
+ final int tagLen = buffer.getInt();
+ final int nonceLen = buffer.getInt();
+
+ if (tagLen != TAG_LENGTH) {
+ throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH));
+ }
+
+ if (nonceLen != NONCE_LENGTH) {
+ throw new InvalidKeyException(String.format("Initialization vector (IV) length must be %s", NONCE_LENGTH));
+ }
+
+ final byte[] nonce = new byte[nonceLen];
+ buffer.get(nonce);
+
+ return new GCMParameterSpec(tagLen, nonce);
+ }
+
+ @Override
+ WrappingData buildWrappingCipher(final Key key, final Map encryptionContext)
+ throws GeneralSecurityException {
+ final byte[] nonce = new byte[NONCE_LENGTH];
+ Utils.getSecureRandom().nextBytes(nonce);
+ final GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, nonce);
+ final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, key, spec);
+ final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext);
+ cipher.updateAAD(aad);
+ return new WrappingData(cipher, specToBytes(spec));
+ }
+
+ @Override
+ Cipher buildUnwrappingCipher(final Key key, final byte[] extraInfo, final int offset,
+ final Map encryptionContext) throws GeneralSecurityException {
+ final GCMParameterSpec spec = bytesToSpec(extraInfo, offset);
+ final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext);
+ cipher.updateAAD(aad);
+ return cipher;
+ }
+}
diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java
new file mode 100644
index 000000000..643278a71
--- /dev/null
+++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java
@@ -0,0 +1,136 @@
+/*
+ * 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.internal;
+
+import com.amazonaws.encryptionsdk.EncryptedDataKey;
+import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
+import com.amazonaws.encryptionsdk.model.KeyBlob;
+import org.apache.commons.lang3.ArrayUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Map;
+
+/**
+ * Abstract class for encrypting and decrypting JCE data keys.
+ */
+public abstract class JceKeyCipher {
+
+ private final Key wrappingKey;
+ private final Key unwrappingKey;
+ private static final Charset KEY_NAME_ENCODING = StandardCharsets.UTF_8;
+
+ /**
+ * Returns a new instance of a JceKeyCipher based on the
+ * Advanced Encryption Standard in Galois/Counter Mode.
+ *
+ * @param secretKey The secret key to use for encrypt/decrypt operations.
+ * @return The JceKeyCipher.
+ */
+ public static JceKeyCipher aesGcm(SecretKey secretKey) {
+ return new AesGcmJceKeyCipher(secretKey);
+ }
+
+ /**
+ * Returns a new instance of a JceKeyCipher based on RSA.
+ *
+ * @param wrappingKey The public key to use for encrypting the key.
+ * @param unwrappingKey The private key to use for decrypting the key.
+ * @param transformation The transformation.
+ * @return The JceKeyCipher.
+ */
+ public static JceKeyCipher rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, String transformation) {
+ return new RsaJceKeyCipher(wrappingKey, unwrappingKey, transformation);
+ }
+
+ JceKeyCipher(Key wrappingKey, Key unwrappingKey) {
+ this.wrappingKey = wrappingKey;
+ this.unwrappingKey = unwrappingKey;
+ }
+
+ abstract WrappingData buildWrappingCipher(Key key, Map encryptionContext) throws GeneralSecurityException;
+
+ abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset,
+ Map encryptionContext) throws GeneralSecurityException;
+
+
+ /**
+ * Encrypts the given key, incorporating the given keyName and encryptionContext.
+ * @param key The key to encrypt.
+ * @param keyName A UTF-8 encoded representing a name for the key.
+ * @param keyNamespace A UTF-8 encoded value that namespaces the key.
+ * @param encryptionContext A key-value mapping of arbitrary, non-secret, UTF-8 encoded strings used
+ * during encryption and decryption to provide additional authenticated data (AAD).
+ * @return The encrypted data key.
+ */
+ public EncryptedDataKey encryptKey(final byte[] key, final String keyName, final String keyNamespace,
+ final Map encryptionContext) {
+
+ final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING);
+
+ try {
+ final JceKeyCipher.WrappingData wData = buildWrappingCipher(wrappingKey, encryptionContext);
+ final Cipher cipher = wData.cipher;
+ final byte[] encryptedKey = cipher.doFinal(key);
+
+ final byte[] provInfo;
+ if (wData.extraInfo.length == 0) {
+ provInfo = keyNameBytes;
+ } else {
+ provInfo = new byte[keyNameBytes.length + wData.extraInfo.length];
+ System.arraycopy(keyNameBytes, 0, provInfo, 0, keyNameBytes.length);
+ System.arraycopy(wData.extraInfo, 0, provInfo, keyNameBytes.length, wData.extraInfo.length);
+ }
+
+ return new KeyBlob(keyNamespace, provInfo, encryptedKey);
+ } catch (final GeneralSecurityException gsex) {
+ throw new AwsCryptoException(gsex);
+ }
+ }
+
+ /**
+ * Decrypts the given encrypted data key.
+ *
+ * @param edk The encrypted data key.
+ * @param keyName A UTF-8 encoded String representing a name for the key.
+ * @param encryptionContext A key-value mapping of arbitrary, non-secret, UTF-8 encoded strings used
+ * during encryption and decryption to provide additional authenticated data (AAD).
+ * @return The decrypted key.
+ * @throws GeneralSecurityException If a problem occurred decrypting the key.
+ */
+ public byte[] decryptKey(final EncryptedDataKey edk, final String keyName,
+ final Map encryptionContext) throws GeneralSecurityException {
+ final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING);
+
+ final Cipher cipher = buildUnwrappingCipher(unwrappingKey, edk.getProviderInformation(),
+ keyNameBytes.length, encryptionContext);
+ return cipher.doFinal(edk.getEncryptedDataKey());
+ }
+
+ static class WrappingData {
+ public final Cipher cipher;
+ public final byte[] extraInfo;
+
+ WrappingData(final Cipher cipher, final byte[] extraInfo) {
+ this.cipher = cipher;
+ this.extraInfo = extraInfo != null ? extraInfo : ArrayUtils.EMPTY_BYTE_ARRAY;
+ }
+ }
+}
diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java
new file mode 100644
index 000000000..c830f5487
--- /dev/null
+++ b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java
@@ -0,0 +1,109 @@
+/*
+ * 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.internal;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A JceKeyCipher based on RSA.
+ */
+class RsaJceKeyCipher extends JceKeyCipher {
+
+ private static final Logger LOGGER = Logger.getLogger(RsaJceKeyCipher.class.getName());
+ // MGF1 with SHA-224 isn't really supported, but we include it in the regex because we need it
+ // for proper handling of the algorithm.
+ private static final Pattern SUPPORTED_TRANSFORMATIONS =
+ Pattern.compile("RSA/ECB/(?:PKCS1Padding|OAEPWith(SHA-(?:1|224|256|384|512))AndMGF1Padding)",
+ Pattern.CASE_INSENSITIVE);
+ private final AlgorithmParameterSpec parameterSpec_;
+ private final String transformation_;
+
+ RsaJceKeyCipher(PublicKey wrappingKey, PrivateKey unwrappingKey, String transformation) {
+ super(wrappingKey, unwrappingKey);
+
+ final Matcher matcher = SUPPORTED_TRANSFORMATIONS.matcher(transformation);
+ if (matcher.matches()) {
+ final String hashUnknownCase = matcher.group(1);
+ if (hashUnknownCase != null) {
+ // OAEP mode a.k.a PKCS #1v2
+ final String hash = hashUnknownCase.toUpperCase();
+ transformation_ = "RSA/ECB/OAEPPadding";
+
+ final MGF1ParameterSpec mgf1Spec;
+ switch (hash) {
+ case "SHA-1":
+ mgf1Spec = MGF1ParameterSpec.SHA1;
+ break;
+ case "SHA-224":
+ LOGGER.warning(transformation + " is not officially supported by the JceMasterKey");
+ mgf1Spec = MGF1ParameterSpec.SHA224;
+ break;
+ case "SHA-256":
+ mgf1Spec = MGF1ParameterSpec.SHA256;
+ break;
+ case "SHA-384":
+ mgf1Spec = MGF1ParameterSpec.SHA384;
+ break;
+ case "SHA-512":
+ mgf1Spec = MGF1ParameterSpec.SHA512;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported algorithm: " + transformation);
+ }
+ parameterSpec_ = new OAEPParameterSpec(hash, "MGF1", mgf1Spec, PSource.PSpecified.DEFAULT);
+ } else {
+ // PKCS #1 v1.x
+ transformation_ = transformation;
+ parameterSpec_ = null;
+ }
+ } else {
+ LOGGER.warning(transformation + " is not officially supported by the JceMasterKey");
+ // Unsupported transformation, just use exactly what we are given
+ transformation_ = transformation;
+ parameterSpec_ = null;
+ }
+ }
+
+ @Override
+ WrappingData buildWrappingCipher(Key key, Map encryptionContext) throws GeneralSecurityException {
+ final Cipher cipher = Cipher.getInstance(transformation_);
+ cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec_);
+ return new WrappingData(cipher, ArrayUtils.EMPTY_BYTE_ARRAY);
+ }
+
+ @Override
+ Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, Map encryptionContext) throws GeneralSecurityException {
+ if (extraInfo.length != offset) {
+ throw new IllegalArgumentException("Extra info must be empty for RSA keys");
+ }
+
+ final Cipher cipher = Cipher.getInstance(transformation_);
+ cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_);
+ return cipher;
+ }
+}
diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java
index adedea54a..5761d03f9 100644
--- a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java
+++ b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java
@@ -311,4 +311,25 @@ public static byte[] bigIntegerToByteArray(final BigInteger bigInteger, final in
System.arraycopy(rawBytes, 0, paddedResult, length - rawBytes.length, rawBytes.length);
return paddedResult;
}
+
+ /**
+ * Returns true if the prefix of the given length for the input arrays are equal.
+ * This method will return as soon as the first difference is found, and is thus not constant-time.
+ *
+ * @param a The first array.
+ * @param b The second array.
+ * @param length The length of the prefix to compare.
+ * @return True if the prefixes are equal, false otherwise.
+ */
+ public static boolean arrayPrefixEquals(final byte[] a, final byte[] b, final int length) {
+ if (a == null || b == null || a.length < length || b.length < length) {
+ return false;
+ }
+ for (int x = 0; x < length; x++) {
+ if (a[x] != b[x]) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java
index 70d289ddd..4995066cb 100644
--- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java
+++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java
@@ -13,58 +13,36 @@
package com.amazonaws.encryptionsdk.jce;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
+import com.amazonaws.encryptionsdk.CryptoAlgorithm;
+import com.amazonaws.encryptionsdk.DataKey;
+import com.amazonaws.encryptionsdk.EncryptedDataKey;
+import com.amazonaws.encryptionsdk.MasterKey;
+import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
+import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException;
+import com.amazonaws.encryptionsdk.internal.JceKeyCipher;
+import com.amazonaws.encryptionsdk.internal.Utils;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.OAEPParameterSpec;
-import javax.crypto.spec.PSource;
-import javax.crypto.spec.SecretKeySpec;
-
-import com.amazonaws.encryptionsdk.CryptoAlgorithm;
-import com.amazonaws.encryptionsdk.DataKey;
-import com.amazonaws.encryptionsdk.EncryptedDataKey;
-import com.amazonaws.encryptionsdk.MasterKey;
-import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
-import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException;
-import com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer;
/**
* 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)}.
*/
-public abstract class JceMasterKey extends MasterKey {
- private static final Logger LOGGER = Logger.getLogger(JceMasterKey.class.getName());
- private static final byte[] EMPTY_ARRAY = new byte[0];
-
- private final SecureRandom rnd = new SecureRandom();
- private final Key wrappingKey_;
- private final Key unwrappingKey_;
+public class JceMasterKey extends MasterKey {
private final String providerName_;
private final String keyId_;
private final byte[] keyIdBytes_;
+ private final JceKeyCipher jceKeyCipher_;
/**
* Returns a {@code JceMasterKey} backed by {@code key} using {@code wrappingAlgorithm}.
@@ -82,7 +60,7 @@ public static JceMasterKey getInstance(final SecretKey key, final String provide
final String wrappingAlgorithm) {
switch (wrappingAlgorithm.toUpperCase()) {
case "AES/GCM/NOPADDING":
- return new AesGcm(key, provider, keyId);
+ return new JceMasterKey(provider, keyId, JceKeyCipher.aesGcm(key));
default:
throw new IllegalArgumentException("Right now only AES/GCM/NoPadding is supported");
@@ -104,18 +82,16 @@ public static JceMasterKey getInstance(final PublicKey wrappingKey, final Privat
final String provider, final String keyId,
final String wrappingAlgorithm) {
if (wrappingAlgorithm.toUpperCase().startsWith("RSA/ECB/")) {
- return new Rsa(wrappingKey, unwrappingKey, provider, keyId, wrappingAlgorithm);
+ return new JceMasterKey(provider, keyId, JceKeyCipher.rsa(wrappingKey, unwrappingKey, wrappingAlgorithm));
}
throw new UnsupportedOperationException("Currently only RSA asymmetric algorithms are supported");
}
- protected JceMasterKey(final Key wrappingKey, final Key unwrappingKey, final String providerName,
- final String keyId) {
- wrappingKey_ = wrappingKey;
- unwrappingKey_ = unwrappingKey;
+ protected JceMasterKey(final String providerName, final String keyId, final JceKeyCipher jceKeyCipher) {
providerName_ = providerName;
keyId_ = keyId;
keyIdBytes_ = keyId_.getBytes(StandardCharsets.UTF_8);
+ jceKeyCipher_ = jceKeyCipher;
}
@Override
@@ -132,9 +108,10 @@ public String getKeyId() {
public DataKey generateDataKey(final CryptoAlgorithm algorithm,
final Map encryptionContext) {
final byte[] rawKey = new byte[algorithm.getDataKeyLength()];
- rnd.nextBytes(rawKey);
- final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo());
- return encryptRawKey(key, rawKey, encryptionContext);
+ Utils.getSecureRandom().nextBytes(rawKey);
+ EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(rawKey, keyId_, providerName_, encryptionContext);
+ return new DataKey<>(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()),
+ encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this);
}
@Override
@@ -150,26 +127,8 @@ public DataKey encryptDataKey(final CryptoAlgorithm algorithm,
throw new IllegalArgumentException("Incorrect key algorithm. Expected " + key.getAlgorithm()
+ " but got " + algorithm.getKeyAlgo());
}
- final byte[] rawKey = key.getEncoded();
- final DataKey result = encryptRawKey(key, rawKey, encryptionContext);
- Arrays.fill(rawKey, (byte) 0);
- return result;
- }
-
- protected DataKey encryptRawKey(final SecretKey key, final byte[] rawKey,
- final Map encryptionContext) {
- try {
- final WrappingData wData = buildWrappingCipher(wrappingKey_, encryptionContext);
- final Cipher cipher = wData.cipher;
- final byte[] encryptedKey = cipher.doFinal(rawKey);
-
- final byte[] provInfo = new byte[keyIdBytes_.length + wData.extraInfo.length];
- System.arraycopy(keyIdBytes_, 0, provInfo, 0, keyIdBytes_.length);
- System.arraycopy(wData.extraInfo, 0, provInfo, keyIdBytes_.length, wData.extraInfo.length);
- return new DataKey<>(key, encryptedKey, provInfo, this);
- } catch (final GeneralSecurityException gsex) {
- throw new AwsCryptoException(gsex);
- }
+ EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key.getEncoded(), keyId_, providerName_, encryptionContext);
+ return new DataKey<>(key, encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this);
}
@Override
@@ -182,10 +141,13 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm,
for (final EncryptedDataKey edk : encryptedDataKeys) {
try {
if (edk.getProviderId().equals(getProviderId())
- && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) {
- final DataKey result = actualDecrypt(algorithm, edk, encryptionContext);
- if (result != null) {
- return result;
+ && Utils.arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) {
+ final byte[] decryptedKey = jceKeyCipher_.decryptKey(edk, keyId_, encryptionContext);
+
+ // Validate that the decrypted key length is as expected
+ if (decryptedKey.length == algorithm.getDataKeyLength()) {
+ return new DataKey<>(new SecretKeySpec(decryptedKey, algorithm.getDataKeyAlgo()),
+ edk.getEncryptedDataKey(), edk.getProviderInformation(), this);
}
}
} catch (final Exception ex) {
@@ -194,194 +156,4 @@ && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.leng
}
throw buildCannotDecryptDksException(exceptions);
}
-
- protected DataKey actualDecrypt(final CryptoAlgorithm algorithm, final EncryptedDataKey edk,
- final Map encryptionContext) throws GeneralSecurityException {
- final Cipher cipher = buildUnwrappingCipher(unwrappingKey_, edk.getProviderInformation(),
- keyIdBytes_.length,
- encryptionContext);
- final byte[] rawKey = cipher.doFinal(edk.getEncryptedDataKey());
- if (rawKey.length != algorithm.getDataKeyLength()) {
- // Something's wrong here. Assume that the decryption is invalid.
- return null;
- }
- return new DataKey<>(
- new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()),
- edk.getEncryptedDataKey(),
- edk.getProviderInformation(), this);
-
- }
-
- protected static boolean arrayPrefixEquals(final byte[] a, final byte[] b, final int len) {
- if (a == null || b == null || a.length < len || b.length < len) {
- return false;
- }
- for (int x = 0; x < len; x++) {
- if (a[x] != b[x]) {
- return false;
- }
- }
- return true;
- }
-
- protected abstract WrappingData buildWrappingCipher(Key key, Map encryptionContext)
- throws GeneralSecurityException;
-
- protected abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset,
- Map encryptionContext) throws GeneralSecurityException;
-
- private static class WrappingData {
- public final Cipher cipher;
- public final byte[] extraInfo;
-
- public WrappingData(final Cipher cipher, final byte[] extraInfo) {
- super();
- this.cipher = cipher;
- this.extraInfo = extraInfo != null ? extraInfo : EMPTY_ARRAY;
- }
- }
-
- private static class Rsa extends JceMasterKey {
- // MGF1 with SHA-224 isn't really supported, but we include it in the regex because we need it
- // for proper handling of the algorithm.
- private static final Pattern SUPPORTED_TRANSFORMATIONS =
- Pattern.compile("RSA/ECB/(?:PKCS1Padding|OAEPWith(SHA-(?:1|224|256|384|512))AndMGF1Padding)",
- Pattern.CASE_INSENSITIVE);
- private final AlgorithmParameterSpec parameterSpec_;
- private final String transformation_;
-
- private Rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, String providerName, String keyId,
- String transformation) {
- super(wrappingKey, unwrappingKey, providerName, keyId);
-
- final Matcher matcher = SUPPORTED_TRANSFORMATIONS.matcher(transformation);
- if (matcher.matches()) {
- final String hashUnknownCase = matcher.group(1);
- if (hashUnknownCase != null) {
- // OAEP mode a.k.a PKCS #1v2
- final String hash = hashUnknownCase.toUpperCase();
- transformation_ = "RSA/ECB/OAEPPadding";
-
- final MGF1ParameterSpec mgf1Spec;
- switch (hash) {
- case "SHA-1":
- mgf1Spec = MGF1ParameterSpec.SHA1;
- break;
- case "SHA-224":
- LOGGER.warning(transformation + " is not officially supported by the JceMasterKey");
- mgf1Spec = MGF1ParameterSpec.SHA224;
- break;
- case "SHA-256":
- mgf1Spec = MGF1ParameterSpec.SHA256;
- break;
- case "SHA-384":
- mgf1Spec = MGF1ParameterSpec.SHA384;
- break;
- case "SHA-512":
- mgf1Spec = MGF1ParameterSpec.SHA512;
- break;
- default:
- throw new IllegalArgumentException("Unsupported algorithm: " + transformation);
- }
- parameterSpec_ = new OAEPParameterSpec(hash, "MGF1", mgf1Spec, PSource.PSpecified.DEFAULT);
- } else {
- // PKCS #1 v1.x
- transformation_ = transformation;
- parameterSpec_ = null;
- }
- } else {
- LOGGER.warning(transformation + " is not officially supported by the JceMasterKey");
- // Unsupported transformation, just use exactly what we are given
- transformation_ = transformation;
- parameterSpec_ = null;
- }
- }
-
- @Override
- protected WrappingData buildWrappingCipher(Key key, Map encryptionContext)
- throws GeneralSecurityException {
- // We require BouncyCastle to avoid some bugs in the default Java implementation
- // of OAEP.
- final Cipher cipher = Cipher.getInstance(transformation_);
- cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec_);
- return new WrappingData(cipher, EMPTY_ARRAY);
- }
-
- @Override
- protected Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset,
- Map encryptionContext) throws GeneralSecurityException {
- if (extraInfo.length != offset) {
- throw new IllegalArgumentException("Extra info must be empty for RSA keys");
- }
- // We require BouncyCastle to avoid some bugs in the default Java implementation
- // of OAEP.
- final Cipher cipher = Cipher.getInstance(transformation_);
- cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_);
- return cipher;
- }
- }
-
- private static class AesGcm extends JceMasterKey {
- private static final int NONCE_LENGTH = 12;
- private static final int TAG_LENGTH = 128;
- private static final String TRANSFORMATION = "AES/GCM/NoPadding";
-
- private final SecureRandom rnd = new SecureRandom();
-
- public AesGcm(final SecretKey key, final String providerName, final String keyId) {
- super(key, key, providerName, keyId);
- }
-
- private static byte[] specToBytes(final GCMParameterSpec spec) {
- final byte[] nonce = spec.getIV();
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try (final DataOutputStream dos = new DataOutputStream(baos)) {
- dos.writeInt(spec.getTLen());
- dos.writeInt(nonce.length);
- dos.write(nonce);
- dos.close();
- baos.close();
- } catch (final IOException ex) {
- throw new AssertionError("Impossible exception", ex);
- }
- return baos.toByteArray();
- }
-
- private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) {
- final ByteArrayInputStream bais = new ByteArrayInputStream(data, offset, data.length - offset);
- try (final DataInputStream dis = new DataInputStream(bais)) {
- final int tagLen = dis.readInt();
- final int nonceLen = dis.readInt();
- final byte[] nonce = new byte[nonceLen];
- dis.readFully(nonce);
- return new GCMParameterSpec(tagLen, nonce);
- } catch (final IOException ex) {
- throw new AssertionError("Impossible exception", ex);
- }
- }
-
- @Override
- protected WrappingData buildWrappingCipher(final Key key, final Map encryptionContext)
- throws GeneralSecurityException {
- final byte[] nonce = new byte[NONCE_LENGTH];
- rnd.nextBytes(nonce);
- final GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, nonce);
- final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
- cipher.init(Cipher.ENCRYPT_MODE, key, spec);
- final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext);
- cipher.updateAAD(aad);
- return new WrappingData(cipher, specToBytes(spec));
- }
-
- @Override
- protected Cipher buildUnwrappingCipher(final Key key, final byte[] extraInfo, final int offset,
- final Map encryptionContext) throws GeneralSecurityException {
- final GCMParameterSpec spec = bytesToSpec(extraInfo, offset);
- final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
- cipher.init(Cipher.DECRYPT_MODE, key, spec);
- final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext);
- cipher.updateAAD(aad);
- return cipher;
- }
- }
}
diff --git a/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java
new file mode 100644
index 000000000..5d162679d
--- /dev/null
+++ b/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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/encryptionsdk/UtilsTest.java b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java
index 50987611f..7a2013023 100644
--- a/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java
+++ b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java
@@ -2,6 +2,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
@@ -121,5 +122,17 @@ public void testBigIntegerToByteArray_InvalidLength() {
Utils.bigIntegerToByteArray(new BigInteger(bytes), 3));
}
+ @Test
+ public void testArrayPrefixEquals() {
+ byte[] a = new byte[] {10, 11, 12, 13, 14, 15};
+ byte[] b = new byte[] {10, 11, 12, 13, 20, 21, 22};
+
+ assertFalse(Utils.arrayPrefixEquals(null, b, 4));
+ assertFalse(Utils.arrayPrefixEquals(a, null, 4));
+ assertFalse(Utils.arrayPrefixEquals(a, b, a.length + 1));
+ assertTrue(Utils.arrayPrefixEquals(a, b, 4));
+ assertFalse(Utils.arrayPrefixEquals(a, b, 5));
+ }
+
}
diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java
index 486c52579..1cd53370b 100644
--- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java
+++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java
@@ -1,6 +1,6 @@
package com.amazonaws.encryptionsdk.kms;
-final class KMSTestFixtures {
+public final class KMSTestFixtures {
private KMSTestFixtures() {
throw new UnsupportedOperationException(
"This class exists to hold static constants and cannot be instantiated."
@@ -14,7 +14,7 @@ private KMSTestFixtures() {
* This should go without saying, but never use these keys for production purposes (as anyone in the world can
* decrypt data encrypted using them).
*/
- static final String[] TEST_KEY_IDS = new String[] {
+ public static final String[] TEST_KEY_IDS = new String[] {
"arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f",
"arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2"
};