Skip to content

Defining the KMS Keyring #147

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

Merged
merged 11 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 AwsCryptoException {

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);
}
}
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 a region that is not allowed to be used by
* a given KmsClientSupplier is specified.
*/
public class UnsupportedRegionException extends AwsCryptoException {

private static final long serialVersionUID = -1L;

public UnsupportedRegionException() {
super();
}

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

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

public UnsupportedRegionException(final String message, final Throwable cause) {
super(message, cause);
}
}
176 changes: 176 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,176 @@
/*
* 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.HashSet;
import java.util.List;
import java.util.Set;

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).
*/
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(new ArrayList<>(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 (generatorKeyId != null) {
// 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));
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we determine whether this should be the keyId the user inputs, or the keyId returned from the KMS result (and is it in the spec/do we have an issue cut to update the spec)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we were leaning toward the keyId the user inputs, which is what I went with, but it wasn't settled. Either way, the spec still just says "generator" which is wrong, I've created an issue: awslabs/aws-encryption-sdk-specification#60

}

@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;
}

final Set<String> configuredKeyIds = new HashSet<>(keyIds);

if (generatorKeyId != null) {
configuredKeyIds.add(generatorKeyId);
}

for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) {
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 a potential O(n^2) operation. We iterate over each EDK and then in okToDecrypt iterate over every keyId we have configured. This seems like a potentially inefficient way to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll put all the configured keys into a HashSet to improve performance

if (okToDecrypt(encryptedDataKey, configuredKeyIds)) {
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, Set<String> configuredKeyIds) {
// Only attempt to decrypt keys provided by KMS
if (!encryptedDataKey.getProviderId().equals(KMS_PROVIDER_ID)) {
return false;
}

// If the key ARN cannot be parsed, skip it
if(!isArnWellFormed(new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING)))
{
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;
}

// 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 configuredKeyIds.contains(new String(encryptedDataKey.getProviderInformation(), PROVIDER_ENCODING));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

package com.amazonaws.encryptionsdk.keyrings;

import com.amazonaws.encryptionsdk.kms.DataKeyEncryptionDao;
import com.amazonaws.encryptionsdk.kms.KmsClientSupplier;

import javax.crypto.SecretKey;
import java.security.PrivateKey;
import java.security.PublicKey;
Expand Down Expand Up @@ -56,6 +59,22 @@ public static Keyring rawRsa(String keyNamespace, String keyName, PublicKey publ
return new RawRsaKeyring(keyNamespace, keyName, publicKey, privateKey, wrappingAlgorithm);
}

/**
* Constructs a {@code Keyring} which interacts with AWS Key Management Service (KMS) to create,
* encrypt, and decrypt data keys using KMS defined Customer Master Keys (CMKs).
*
* @param clientSupplier A function that returns a KMS client that can make GenerateDataKey,
* Encrypt, and Decrypt calls in a particular AWS region.
* @param grantTokens A list of string grant tokens to be included in all KMS calls.
* @param keyIds A list of strings identifying KMS CMKs, in ARN, CMK Alias, or ARN Alias format.
* @param generator A string that identifies a KMS CMK responsible for generating a data key,
* as well as encrypting and decrypting data keys in ARN, CMK Alias, or ARN Alias format.
* @return The {@code Keyring}
*/
public static Keyring kms(KmsClientSupplier clientSupplier, List<String> grantTokens, List<String> keyIds, String generator) {
return new KmsKeyring(DataKeyEncryptionDao.kms(clientSupplier, grantTokens), keyIds, generator);
}

/**
* Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call
* to modify the encryption or decryption materials using more than one keyring.
Expand Down
Loading