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.amazonaws aws-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.amazonaws aws-encryption-sdk-java - 1.6.0 + 1.6.1 jar aws-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: + *

    + *
  1. 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" };