-
Notifications
You must be signed in to change notification settings - Fork 123
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
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
93f9691
Refactor JceMasterKey to extract logic to be shared by raw keyrings.
WesleyRosenblum 78e956b
Remove requirement that wrappingKey and unwrappingKey be non null
WesleyRosenblum 28ec657
Simplifying JceKeyCipher methods
WesleyRosenblum 66b3838
Changing encryptKey and decryptKey methods to use key bytes instead o…
WesleyRosenblum f789a23
Removed unused imports
WesleyRosenblum 7e2dde6
Adding validation to AesGcm key cipher and moving ArrayPrefixEquals t…
WesleyRosenblum c6bd363
Set provider ID correctly in the encrypted data key.
WesleyRosenblum 9d248fb
Using ByteByffer instead of ByteArrayInput/OutputStreams
WesleyRosenblum 209c7ec
Ensure spec length is correct
WesleyRosenblum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
* 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"; | ||
|
||
AesGcmJceKeyCipher(SecretKey key) { | ||
super(key, key); | ||
} | ||
|
||
private static byte[] specToBytes(final GCMParameterSpec spec) { | ||
final byte[] nonce = spec.getIV(); | ||
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 ByteBuffer buffer = ByteBuffer.wrap(data, offset, data.length - offset); | ||
|
||
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 || 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<String, String> 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<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; | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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; | ||
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<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; | ||
} | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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"); | ||
} | ||
|
||
final Cipher cipher = Cipher.getInstance(transformation_); | ||
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_); | ||
return cipher; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the length is too short, you
BufferUnderflowException
which should be caught and rethrown as an InvalidKeyException.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defined a SPEC_LENGTH constant that I validate against