Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor JceMasterKey to extract logic to be shared by raw keyrings. #139

Merged
merged 9 commits into from
Nov 8, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.InvalidKeyException;
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this and use Utils.getSecureRandom() exactly when you need it. (This avoids both creating more instances than you need and thread-contention.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't see Utils.getSecureRandom until recently and forgot to go back and update this change


AesGcmJceKeyCipher(SecretKey key) {
super(key, key);
}

private static byte[] specToBytes(final GCMParameterSpec spec) {
final byte[] nonce = spec.getIV();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NitPick: Since we know exactly how long this will be, it's probably more efficient to use a ByteBuffer than a ByteArrayOutputStream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to ByteBuffer, it simplifies the code as well

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) throws InvalidKeyException {
final ByteArrayInputStream bais = new ByteArrayInputStream(data, offset, data.length - offset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NitPick: Same comment about ByteBuffer probably being more efficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only part of this PR that isn't a refactor. I made this change to bring this in line with the RawAesKeyring spec


final byte[] nonce = new byte[nonceLen];
dis.readFully(nonce);
return new GCMParameterSpec(tagLen, nonce);
} catch (final IOException ex) {
throw new AssertionError("Impossible exception", ex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really impossible? I suspect that if the data is too short you'll get an IOException. I'd just rethrow it as an InvalidKeyException.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm using a ByteBuffer it is impossible (ByteBuffer doesn't throw IOExceptions). I'm explicitly checking the data size against the nonce length now.

}
}

@Override
WrappingData buildWrappingCipher(final Key key, final Map<String, String> encryptionContext)
throws GeneralSecurityException {
final byte[] nonce = new byte[NONCE_LENGTH];
rnd.nextBytes(nonce);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Utils.getSecureRandom().nextBytes(nonce);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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<String, String> 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;
}
}
131 changes: 131 additions & 0 deletions src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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<String, String> encryptionContext) throws GeneralSecurityException;

abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset,
Map<String, String> 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<String, String> 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 = 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If wData.extraInfo.length==0, then we should short-circuit this to avoid extra data-copies and allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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<String, String> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an old comment and should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted

final Cipher cipher = Cipher.getInstance(transformation_);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_);
return cipher;
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
SalusaSecondus marked this conversation as resolved.
Show resolved Hide resolved
*
* @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;
}
}
Loading