From 93f9691e5b360cf4ea7541b224c6a80665fa8f25 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Thu, 31 Oct 2019 17:12:55 -0700 Subject: [PATCH 1/9] Refactor JceMasterKey to extract logic to be shared by raw keyrings. *Issue #, if available:* #102 *Description of changes:* In anticipation of the RawAesKeyring and RawRsaKeyring needing logic currently embedded in the JceMasterKey, this change extracts that logic into the JceKeyCipher class so it may be shared. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files. --- .../internal/AesGcmJceKeyCipher.java | 93 ++++++ .../encryptionsdk/internal/JceKeyCipher.java | 144 +++++++++ .../internal/RsaJceKeyCipher.java | 110 +++++++ .../encryptionsdk/jce/JceMasterKey.java | 273 ++---------------- 4 files changed, 377 insertions(+), 243 deletions(-) create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java 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..b840a8404 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -0,0 +1,93 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.SecureRandom; +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 final SecureRandom rnd = new SecureRandom(); + + AesGcmJceKeyCipher(SecretKey key) { + super(key, key); + } + + 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 + 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 + 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..043bb6a83 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -0,0 +1,144 @@ +/* + * 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.CryptoAlgorithm; +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; + +import static org.apache.commons.lang3.Validate.notNull; + +/** + * 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) { + notNull(wrappingKey, "wrappingKey is required"); + notNull(wrappingKey, "unwrappingKey is required"); + + 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 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 Key key, final String keyName, + final Map encryptionContext) { + + final byte[] keyBytes = key.getEncoded(); + 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(keyBytes); + + final byte[] 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(keyName, provInfo, encryptedKey); + } catch (final GeneralSecurityException gsex) { + throw new AwsCryptoException(gsex); + } + } + + /** + * Decrypts the given encrypted data key. + * + * @param algorithm The algorithm that encrypted the data key. + * @param edk The encrypted data key. + * @param keyName A UTF-8 encoded 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 KeyBlob decryptKey(final CryptoAlgorithm algorithm, 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); + 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 KeyBlob(edk.getProviderId(), edk.getProviderInformation(), rawKey); + } + + 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..cdb79960a --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.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.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"); + } + // 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; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index 70d289ddd..d106e617a 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -13,58 +13,38 @@ 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.model.KeyBlob; + +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]; - +public class JceMasterKey extends MasterKey { private final SecureRandom rnd = new SecureRandom(); - private final Key wrappingKey_; - private final Key unwrappingKey_; 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 +62,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 +84,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 @@ -133,8 +111,10 @@ 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); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), keyId_, + encryptionContext); + return new DataKey<>(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), + encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @Override @@ -150,26 +130,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, keyId_, encryptionContext); + return new DataKey<>(key, encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @Override @@ -183,9 +145,12 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, try { if (edk.getProviderId().equals(getProviderId()) && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { - final DataKey result = actualDecrypt(algorithm, edk, encryptionContext); - if (result != null) { - return result; + final KeyBlob decryptedKey = jceKeyCipher_.decryptKey(algorithm, edk, keyId_, encryptionContext); + + if(decryptedKey != null) { + return new DataKey<>( + new SecretKeySpec(decryptedKey.getEncryptedDataKey(), algorithm.getDataKeyAlgo()), + edk.getEncryptedDataKey(), edk.getProviderInformation(), this); } } } catch (final Exception ex) { @@ -195,24 +160,7 @@ && 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) { + private 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; } @@ -223,165 +171,4 @@ protected static boolean arrayPrefixEquals(final byte[] a, final byte[] b, final } 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; - } - } } From 78e956bfebc977af109530ea414d832fc78a4f18 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Thu, 31 Oct 2019 18:45:57 -0700 Subject: [PATCH 2/9] Remove requirement that wrappingKey and unwrappingKey be non null --- .../com/amazonaws/encryptionsdk/internal/JceKeyCipher.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index 043bb6a83..28b0904e7 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -29,8 +29,6 @@ import java.security.PublicKey; import java.util.Map; -import static org.apache.commons.lang3.Validate.notNull; - /** * Abstract class for encrypting and decrypting JCE data keys. */ @@ -64,9 +62,6 @@ public static JceKeyCipher rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, } JceKeyCipher(Key wrappingKey, Key unwrappingKey) { - notNull(wrappingKey, "wrappingKey is required"); - notNull(wrappingKey, "unwrappingKey is required"); - this.wrappingKey = wrappingKey; this.unwrappingKey = unwrappingKey; } From 28ec657022db42096dd2a312a5979e359d856153 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Thu, 31 Oct 2019 20:02:59 -0700 Subject: [PATCH 3/9] Simplifying JceKeyCipher methods --- .../com/amazonaws/encryptionsdk/internal/JceKeyCipher.java | 7 ++++--- .../java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java | 7 ++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index 28b0904e7..c39365294 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -21,6 +21,7 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; @@ -80,7 +81,7 @@ abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, * during encryption and decryption to provide additional authenticated data (AAD). * @return The encrypted data key. */ - public EncryptedDataKey encryptKey(final Key key, final String keyName, + public EncryptedDataKey encryptKey(final SecretKey key, final String keyName, final Map encryptionContext) { final byte[] keyBytes = key.getEncoded(); @@ -112,7 +113,7 @@ public EncryptedDataKey encryptKey(final Key key, final String keyName, * @return The decrypted key. * @throws GeneralSecurityException If a problem occurred decrypting the key. */ - public KeyBlob decryptKey(final CryptoAlgorithm algorithm, final EncryptedDataKey edk, final String keyName, + public SecretKey decryptKey(final CryptoAlgorithm algorithm, final EncryptedDataKey edk, final String keyName, final Map encryptionContext) throws GeneralSecurityException { final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING); @@ -124,7 +125,7 @@ public KeyBlob decryptKey(final CryptoAlgorithm algorithm, final EncryptedDataKe return null; } - return new KeyBlob(edk.getProviderId(), edk.getProviderInformation(), rawKey); + return new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); } static class WrappingData { diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index d106e617a..8ab9e6efc 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -20,7 +20,6 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; import com.amazonaws.encryptionsdk.internal.JceKeyCipher; -import com.amazonaws.encryptionsdk.model.KeyBlob; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -145,12 +144,10 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, try { if (edk.getProviderId().equals(getProviderId()) && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { - final KeyBlob decryptedKey = jceKeyCipher_.decryptKey(algorithm, edk, keyId_, encryptionContext); + final SecretKey decryptedKey = jceKeyCipher_.decryptKey(algorithm, edk, keyId_, encryptionContext); if(decryptedKey != null) { - return new DataKey<>( - new SecretKeySpec(decryptedKey.getEncryptedDataKey(), algorithm.getDataKeyAlgo()), - edk.getEncryptedDataKey(), edk.getProviderInformation(), this); + return new DataKey<>(decryptedKey, edk.getEncryptedDataKey(), edk.getProviderInformation(), this); } } } catch (final Exception ex) { From 66b38380a27346e82f592a3c7c35fdf833b9a487 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Mon, 4 Nov 2019 16:03:28 -0800 Subject: [PATCH 4/9] Changing encryptKey and decryptKey methods to use key bytes instead of SecretKey --- .../encryptionsdk/internal/JceKeyCipher.java | 16 ++++------------ .../encryptionsdk/jce/JceMasterKey.java | 13 +++++++------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index c39365294..86840b9d4 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -81,16 +81,15 @@ abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, * during encryption and decryption to provide additional authenticated data (AAD). * @return The encrypted data key. */ - public EncryptedDataKey encryptKey(final SecretKey key, final String keyName, + public EncryptedDataKey encryptKey(final byte[] key, final String keyName, final Map encryptionContext) { - final byte[] keyBytes = key.getEncoded(); 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(keyBytes); + final byte[] encryptedKey = cipher.doFinal(key); final byte[] provInfo = new byte[keyNameBytes.length + wData.extraInfo.length]; System.arraycopy(keyNameBytes, 0, provInfo, 0, keyNameBytes.length); @@ -105,7 +104,6 @@ public EncryptedDataKey encryptKey(final SecretKey key, final String keyName, /** * Decrypts the given encrypted data key. * - * @param algorithm The algorithm that encrypted the data key. * @param edk The encrypted data key. * @param keyName A UTF-8 encoded representing a name for the key. * @param encryptionContext A key-value mapping of arbitrary, non-secret, UTF-8 encoded strings used @@ -113,19 +111,13 @@ public EncryptedDataKey encryptKey(final SecretKey key, final String keyName, * @return The decrypted key. * @throws GeneralSecurityException If a problem occurred decrypting the key. */ - public SecretKey decryptKey(final CryptoAlgorithm algorithm, final EncryptedDataKey edk, final String keyName, + 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); - 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 SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); + return cipher.doFinal(edk.getEncryptedDataKey()); } static class WrappingData { diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index 8ab9e6efc..2c7350ed8 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -110,8 +110,7 @@ public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; rnd.nextBytes(rawKey); - EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), keyId_, - encryptionContext); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(rawKey, keyId_, encryptionContext); return new DataKey<>(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @@ -129,7 +128,7 @@ public DataKey encryptDataKey(final CryptoAlgorithm algorithm, throw new IllegalArgumentException("Incorrect key algorithm. Expected " + key.getAlgorithm() + " but got " + algorithm.getKeyAlgo()); } - EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key, keyId_, encryptionContext); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key.getEncoded(), keyId_, encryptionContext); return new DataKey<>(key, encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @@ -144,10 +143,12 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, try { if (edk.getProviderId().equals(getProviderId()) && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { - final SecretKey decryptedKey = jceKeyCipher_.decryptKey(algorithm, edk, keyId_, encryptionContext); + final byte[] decryptedKey = jceKeyCipher_.decryptKey(edk, keyId_, encryptionContext); - if(decryptedKey != null) { - return new DataKey<>(decryptedKey, edk.getEncryptedDataKey(), edk.getProviderInformation(), this); + // 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) { From f789a2359eeb846af68d09986e7e13eb298f144d Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Mon, 4 Nov 2019 16:06:20 -0800 Subject: [PATCH 5/9] Removed unused imports --- .../java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index 86840b9d4..b49eebbbb 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -13,7 +13,6 @@ package com.amazonaws.encryptionsdk.internal; -import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.EncryptedDataKey; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.model.KeyBlob; @@ -21,7 +20,6 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; From 7e2dde65f24f0478e1b06ae6728a0af408145b02 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Tue, 5 Nov 2019 12:58:51 -0800 Subject: [PATCH 6/9] Adding validation to AesGcm key cipher and moving ArrayPrefixEquals to Utils --- .../internal/AesGcmJceKeyCipher.java | 12 ++++++++++- .../encryptionsdk/internal/Utils.java | 20 +++++++++++++++++++ .../encryptionsdk/jce/JceMasterKey.java | 15 ++------------ .../amazonaws/encryptionsdk/UtilsTest.java | 13 ++++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java index b840a8404..617cd8408 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -22,6 +22,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; import java.security.Key; import java.security.SecureRandom; import java.util.Map; @@ -54,11 +55,20 @@ private static byte[] specToBytes(final GCMParameterSpec spec) { return baos.toByteArray(); } - private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) { + private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws GeneralSecurityException { 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(); + + 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]; dis.readFully(nonce); return new GCMParameterSpec(tagLen, nonce); diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java index adedea54a..11490f3ee 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java @@ -311,4 +311,24 @@ 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. + * + * @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 2c7350ed8..5773c0a1c 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -20,6 +20,7 @@ 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; @@ -142,7 +143,7 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, for (final EncryptedDataKey edk : encryptedDataKeys) { try { if (edk.getProviderId().equals(getProviderId()) - && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { + && Utils.arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { final byte[] decryptedKey = jceKeyCipher_.decryptKey(edk, keyId_, encryptionContext); // Validate that the decrypted key length is as expected @@ -157,16 +158,4 @@ && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.leng } throw buildCannotDecryptDksException(exceptions); } - - private 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; - } } 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)); + } + } From c6bd3638d6e12057eeac792313c5fbe618a54080 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Tue, 5 Nov 2019 16:06:09 -0800 Subject: [PATCH 7/9] Set provider ID correctly in the encrypted data key. --- .../encryptionsdk/internal/AesGcmJceKeyCipher.java | 6 +++--- .../com/amazonaws/encryptionsdk/internal/JceKeyCipher.java | 7 ++++--- .../java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java index 617cd8408..5e01bdce4 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -55,17 +55,17 @@ private static byte[] specToBytes(final GCMParameterSpec spec) { return baos.toByteArray(); } - private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws GeneralSecurityException { + private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws InvalidKeyException { 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(); - if(tagLen != TAG_LENGTH) { + if (tagLen != TAG_LENGTH) { throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH)); } - if(nonceLen != NONCE_LENGTH) { + if (nonceLen != NONCE_LENGTH) { throw new InvalidKeyException(String.format("Initialization vector (IV) length must be %s", NONCE_LENGTH)); } diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index b49eebbbb..916034a53 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -75,11 +75,12 @@ abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, * 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, + public EncryptedDataKey encryptKey(final byte[] key, final String keyName, final String keyNamespace, final Map encryptionContext) { final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING); @@ -93,7 +94,7 @@ public EncryptedDataKey encryptKey(final byte[] key, final String keyName, System.arraycopy(keyNameBytes, 0, provInfo, 0, keyNameBytes.length); System.arraycopy(wData.extraInfo, 0, provInfo, keyNameBytes.length, wData.extraInfo.length); - return new KeyBlob(keyName, provInfo, encryptedKey); + return new KeyBlob(keyNamespace, provInfo, encryptedKey); } catch (final GeneralSecurityException gsex) { throw new AwsCryptoException(gsex); } @@ -103,7 +104,7 @@ public EncryptedDataKey encryptKey(final byte[] key, final String keyName, * Decrypts the given encrypted data key. * * @param edk The encrypted data key. - * @param keyName A UTF-8 encoded representing a name for the 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. diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index 5773c0a1c..8d4285833 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -111,7 +111,7 @@ public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; rnd.nextBytes(rawKey); - EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(rawKey, keyId_, encryptionContext); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(rawKey, keyId_, providerName_, encryptionContext); return new DataKey<>(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @@ -129,7 +129,7 @@ public DataKey encryptDataKey(final CryptoAlgorithm algorithm, throw new IllegalArgumentException("Incorrect key algorithm. Expected " + key.getAlgorithm() + " but got " + algorithm.getKeyAlgo()); } - EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key.getEncoded(), keyId_, encryptionContext); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key.getEncoded(), keyId_, providerName_, encryptionContext); return new DataKey<>(key, encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } From 9d248fb24fce0eac5a3f5a3d213214be07bc6aef Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Fri, 8 Nov 2019 13:00:40 -0800 Subject: [PATCH 8/9] Using ByteByffer instead of ByteArrayInput/OutputStreams --- .../internal/AesGcmJceKeyCipher.java | 55 +++++++------------ .../encryptionsdk/internal/JceKeyCipher.java | 11 +++- .../internal/RsaJceKeyCipher.java | 3 +- .../encryptionsdk/internal/Utils.java | 1 + .../encryptionsdk/jce/JceMasterKey.java | 4 +- 5 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java index 5e01bdce4..08b361597 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -16,15 +16,10 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.Key; -import java.security.SecureRandom; import java.util.Map; /** @@ -34,7 +29,6 @@ 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 final SecureRandom rnd = new SecureRandom(); AesGcmJceKeyCipher(SecretKey key) { super(key, key); @@ -42,46 +36,39 @@ class AesGcmJceKeyCipher extends JceKeyCipher { 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(); + final byte[] result = new byte[Integer.BYTES + Integer.BYTES + nonce.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 { - 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 ByteBuffer buffer = ByteBuffer.wrap(data, offset, data.length - offset); - if (tagLen != TAG_LENGTH) { - throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH)); - } + final int tagLen = buffer.getInt(); + final int nonceLen = buffer.getInt(); - if (nonceLen != NONCE_LENGTH) { - throw new InvalidKeyException(String.format("Initialization vector (IV) length must be %s", NONCE_LENGTH)); - } + if (tagLen != TAG_LENGTH) { + throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH)); + } - final byte[] nonce = new byte[nonceLen]; - dis.readFully(nonce); - return new GCMParameterSpec(tagLen, nonce); - } catch (final IOException ex) { - throw new AssertionError("Impossible exception", ex); + if (nonceLen != NONCE_LENGTH || buffer.remaining() != 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]; - rnd.nextBytes(nonce); + 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); diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java index 916034a53..643278a71 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -90,9 +90,14 @@ public EncryptedDataKey encryptKey(final byte[] key, final String keyName, final final Cipher cipher = wData.cipher; final byte[] encryptedKey = cipher.doFinal(key); - final byte[] 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); + 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) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java index cdb79960a..c830f5487 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java @@ -101,8 +101,7 @@ Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, Map { - private final SecureRandom rnd = new SecureRandom(); private final String providerName_; private final String keyId_; private final byte[] keyIdBytes_; @@ -110,7 +108,7 @@ public String getKeyId() { public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; - rnd.nextBytes(rawKey); + 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); From 209c7ececc225a7e98447a8af262bc5a7376112c Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum Date: Fri, 8 Nov 2019 14:00:22 -0800 Subject: [PATCH 9/9] Ensure spec length is correct --- .../encryptionsdk/internal/AesGcmJceKeyCipher.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java index 08b361597..7a4511f01 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -29,6 +29,7 @@ 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); @@ -36,7 +37,7 @@ class AesGcmJceKeyCipher extends JceKeyCipher { private static byte[] specToBytes(final GCMParameterSpec spec) { final byte[] nonce = spec.getIV(); - final byte[] result = new byte[Integer.BYTES + Integer.BYTES + nonce.length]; + final byte[] result = new byte[SPEC_LENGTH]; final ByteBuffer buffer = ByteBuffer.wrap(result); buffer.putInt(spec.getTLen()); buffer.putInt(nonce.length); @@ -45,8 +46,11 @@ private static byte[] specToBytes(final GCMParameterSpec spec) { } private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws InvalidKeyException { - final ByteBuffer buffer = ByteBuffer.wrap(data, offset, data.length - offset); + 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(); @@ -54,7 +58,7 @@ private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH)); } - if (nonceLen != NONCE_LENGTH || buffer.remaining() != NONCE_LENGTH) { + if (nonceLen != NONCE_LENGTH) { throw new InvalidKeyException(String.format("Initialization vector (IV) length must be %s", NONCE_LENGTH)); }