Skip to content

Commit

Permalink
*Issue #, if available:* #102
Browse files Browse the repository at this point in the history
*Description of changes:*

Defining the KMS Keyring.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

# Check any applicable:
- [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files.
  • Loading branch information
WesleyRosenblum committed Nov 22, 2019
1 parent 36958b8 commit 1bef9f5
Show file tree
Hide file tree
Showing 13 changed files with 1,214 additions and 79 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.11.561</version>
<version>1.11.677</version>
<optional>true</optional>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.exception;

/**
* This exception is thrown when an Amazon Resource Name is provided that does not
* match the CMK Alias or ARN format.
*/
public class MalformedArnException extends AwsCryptoException {

private static final long serialVersionUID = -1L;

public MalformedArnException() {
super();
}

public MalformedArnException(final String message) {
super(message);
}

public MalformedArnException(final Throwable cause) {
super(cause);
}

public MalformedArnException(final String message, final Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.exception;

/**
* This exception is thrown when the key used by KMS to decrypt a data key does not
* match the provider information contained within the encrypted data key.
*/
public class MismatchedDataKeyException extends IllegalStateException {

private static final long serialVersionUID = -1L;

public MismatchedDataKeyException() {
super();
}

public MismatchedDataKeyException(final String message) {
super(message);
}

public MismatchedDataKeyException(final Throwable cause) {
super(cause);
}

public MismatchedDataKeyException(final String message, final Throwable cause) {
super(message, cause);
}
}
171 changes: 171 additions & 0 deletions src/main/java/com/amazonaws/encryptionsdk/keyrings/KmsKeyring.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* 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.keyrings;

import com.amazonaws.encryptionsdk.EncryptedDataKey;
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException;
import com.amazonaws.encryptionsdk.exception.MalformedArnException;
import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao;
import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao.DecryptDataKeyResult;
import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao.GenerateDataKeyResult;
import com.amazonaws.encryptionsdk.kms.KmsUtils;

import java.util.ArrayList;
import java.util.List;

import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING;
import static com.amazonaws.encryptionsdk.kms.KmsUtils.KMS_PROVIDER_ID;
import static com.amazonaws.encryptionsdk.kms.KmsUtils.isArnWellFormed;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;

/**
* A keyring which interacts with AWS Key Management Service (KMS) to create,
* encrypt, and decrypt data keys using KMS defined Customer Master Keys (CMKs).
*/
public class KmsKeyring implements Keyring {

private final DataKeyEncryptionDao dataKeyEncryptionDao;
private final List<String> keyIds;
private final String generatorKeyId;
private final boolean isDiscovery;

KmsKeyring(DataKeyEncryptionDao dataKeyEncryptionDao, List<String> keyIds, String generatorKeyId) {
requireNonNull(dataKeyEncryptionDao, "dataKeyEncryptionDao is required");
this.dataKeyEncryptionDao = dataKeyEncryptionDao;
this.keyIds = keyIds == null ? emptyList() : unmodifiableList(keyIds);
this.generatorKeyId = generatorKeyId;
this.isDiscovery = this.generatorKeyId == null && this.keyIds.isEmpty();

if (!this.keyIds.stream().allMatch(KmsUtils::isArnWellFormed)) {
throw new MalformedArnException("keyIds must contain only CMK aliases and well formed ARNs");
}

if (generatorKeyId != null) {
if (!isArnWellFormed(generatorKeyId)) {
throw new MalformedArnException("generatorKeyId must be either a CMK alias or a well formed ARN");
}
if (this.keyIds.contains(generatorKeyId)) {
throw new IllegalArgumentException("KeyIds should not contain the generatorKeyId");
}
}
}

@Override
public void onEncrypt(EncryptionMaterials encryptionMaterials) {
requireNonNull(encryptionMaterials, "encryptionMaterials are required");

// If this keyring is a discovery keyring, OnEncrypt MUST return the input encryption materials unmodified.
if (isDiscovery) {
return;
}

// If the input encryption materials do not contain a plaintext data key and this keyring does not
// have a generator defined, OnEncrypt MUST not modify the encryption materials and MUST fail.
if (!encryptionMaterials.hasPlaintextDataKey() && generatorKeyId == null) {
throw new AwsCryptoException("Encryption materials must contain either a plaintext data key or a generator");
}

final List<String> keyIdsToEncrypt = new ArrayList<>(keyIds);

// If the input encryption materials do not contain a plaintext data key and a generator is defined onEncrypt
// MUST attempt to generate a new plaintext data key and encrypt that data key by calling KMS GenerateDataKey.
if (!encryptionMaterials.hasPlaintextDataKey()) {
generateDataKey(encryptionMaterials);
} else {
// If this keyring's generator is defined and was not used to generate a data key, OnEncrypt
// MUST also attempt to encrypt the plaintext data key using the CMK specified by the generator.
keyIdsToEncrypt.add(generatorKeyId);
}

// Given a plaintext data key in the encryption materials, OnEncrypt MUST attempt
// to encrypt the plaintext data key using each CMK specified in it's key IDs list.
for (String keyId : keyIdsToEncrypt) {
encryptDataKey(keyId, encryptionMaterials);
}
}

private void generateDataKey(final EncryptionMaterials encryptionMaterials) {
final GenerateDataKeyResult result = dataKeyEncryptionDao.generateDataKey(generatorKeyId,
encryptionMaterials.getAlgorithmSuite(), encryptionMaterials.getEncryptionContext());

encryptionMaterials.setPlaintextDataKey(result.getPlaintextDataKey(),
new KeyringTraceEntry(KMS_PROVIDER_ID, generatorKeyId, KeyringTraceFlag.GENERATED_DATA_KEY));
encryptionMaterials.addEncryptedDataKey(result.getEncryptedDataKey(),
new KeyringTraceEntry(KMS_PROVIDER_ID, generatorKeyId, KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT));
}

private void encryptDataKey(final String keyId, final EncryptionMaterials encryptionMaterials) {
final EncryptedDataKey encryptedDataKey = dataKeyEncryptionDao.encryptDataKey(keyId,
encryptionMaterials.getPlaintextDataKey(), encryptionMaterials.getEncryptionContext());

encryptionMaterials.addEncryptedDataKey(encryptedDataKey,
new KeyringTraceEntry(KMS_PROVIDER_ID, keyId, KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT));
}

@Override
public void onDecrypt(DecryptionMaterials decryptionMaterials, List<? extends EncryptedDataKey> encryptedDataKeys) {
requireNonNull(decryptionMaterials, "decryptionMaterials are required");
requireNonNull(encryptedDataKeys, "encryptedDataKeys are required");

if (decryptionMaterials.hasPlaintextDataKey() || encryptedDataKeys.isEmpty()) {
return;
}

if (!encryptedDataKeys.stream()
.filter(edk -> edk.getProviderId().equals(KMS_PROVIDER_ID))
.map(edk -> new String(edk.getProviderInformation(), PROVIDER_ENCODING))
.allMatch(KmsUtils::isArnWellFormed)) {
throw new MalformedArnException("encryptedDataKeys contains a malformed ARN");
}

for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) {
if (okToDecrypt(encryptedDataKey)) {
try {
final DecryptDataKeyResult result = dataKeyEncryptionDao.decryptDataKey(encryptedDataKey,
decryptionMaterials.getAlgorithmSuite(), decryptionMaterials.getEncryptionContext());

decryptionMaterials.setPlaintextDataKey(result.getPlaintextDataKey(),
new KeyringTraceEntry(KMS_PROVIDER_ID, result.getKeyArn(),
KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT));
return;
} catch (CannotUnwrapDataKeyException e) {
continue;
}
}
}
}

private boolean okToDecrypt(EncryptedDataKey encryptedDataKey) {
// Only attempt to decrypt keys provided by KMS
if (!encryptedDataKey.getProviderId().equals(KMS_PROVIDER_ID)) {
return false;
}

// If this keyring is a discovery keyring, OnDecrypt MUST attempt to
// decrypt every encrypted data key in the input encrypted data key list
if (isDiscovery) {
return true;
}

final String providerInfo = new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING);

// OnDecrypt MUST attempt to decrypt each input encrypted data key in the input
// encrypted data key list where the key provider info has a value equal to one
// of the ARNs in this keyring's key IDs or the generator
return providerInfo.equals(generatorKeyId) || keyIds.stream().anyMatch(providerInfo::equals);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.kms;

import com.amazonaws.encryptionsdk.CryptoAlgorithm;
import com.amazonaws.encryptionsdk.EncryptedDataKey;

import javax.crypto.SecretKey;
import java.util.List;
import java.util.Map;

public interface DataKeyEncryptionDao {

/**
* Generates a unique data key, returning both the plaintext copy of the key and an encrypted copy encrypted using
* the customer master key specified by the given keyId.
*
* @param keyId The customer master key to encrypt the generated key with.
* @param algorithmSuite The algorithm suite associated with the key.
* @param encryptionContext The encryption context.
* @return GenerateDataKeyResult containing the plaintext data key and the encrypted data key.
*/
GenerateDataKeyResult generateDataKey(String keyId, CryptoAlgorithm algorithmSuite, Map<String, String> encryptionContext);

/**
* Encrypts the given plaintext data key using the customer aster key specified by the given keyId.
*
* @param keyId The customer master key to encrypt the plaintext data key with.
* @param plaintextDataKey The plaintext data key to encrypt.
* @param encryptionContext The encryption context.
* @return The encrypted data key.
*/
EncryptedDataKey encryptDataKey(final String keyId, SecretKey plaintextDataKey, Map<String, String> encryptionContext);

/**
* Decrypted the given encrypted data key.
*
* @param encryptedDataKey The encrypted data key to decrypt.
* @param algorithmSuite The algorithm suite associated with the key.
* @param encryptionContext The encryption context.
* @return DecryptDataKeyResult containing the plaintext data key and the ARN of the key that decrypted it.
*/
DecryptDataKeyResult decryptDataKey(EncryptedDataKey encryptedDataKey, CryptoAlgorithm algorithmSuite, Map<String, String> encryptionContext);

/**
* Constructs an instance of DataKeyEncryptionDao that uses AWS Key Management Service (KMS) for
* generation, encryption, and decryption of data keys.
*
* @param clientSupplier A supplier of AWSKMS clients
* @param grantTokens A list of grant tokens to supply to KMS
* @return The DataKeyEncryptionDao
*/
static DataKeyEncryptionDao kms(KmsClientSupplier clientSupplier, List<String> grantTokens) {
return new KmsDataKeyEncryptionDao(clientSupplier, grantTokens);
}

class GenerateDataKeyResult {
private final SecretKey plaintextDataKey;
private final EncryptedDataKey encryptedDataKey;

public GenerateDataKeyResult(SecretKey plaintextDataKey, EncryptedDataKey encryptedDataKey) {
this.plaintextDataKey = plaintextDataKey;
this.encryptedDataKey = encryptedDataKey;
}

public SecretKey getPlaintextDataKey() {
return plaintextDataKey;
}

public EncryptedDataKey getEncryptedDataKey() {
return encryptedDataKey;
}
}

class DecryptDataKeyResult {
private final String keyArn;
private final SecretKey plaintextDataKey;

public DecryptDataKeyResult(String keyArn, SecretKey plaintextDataKey) {
this.keyArn = keyArn;
this.plaintextDataKey = plaintextDataKey;
}

public String getKeyArn() {
return keyArn;
}

public SecretKey getPlaintextDataKey() {
return plaintextDataKey;
}

}
}
Loading

0 comments on commit 1bef9f5

Please sign in to comment.