com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.DirectKmsMaterialProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aws-dynamodb-encryption-java Show documentation
Show all versions of aws-dynamodb-encryption-java Show documentation
AWS DynamoDB Encryption Client for AWS Java SDK v1
/*
* Copyright 2014 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.services.dynamodbv2.datamodeling.encryption.providers;
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.CONTENT_KEY_ALGORITHM;
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.ENVELOPE_KEY;
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials.KEY_WRAPPING_ALGORITHM;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.DecryptionMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.EncryptionMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.SymmetricRawMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.WrappedRawMaterials;
import com.amazonaws.services.dynamodbv2.datamodeling.internal.Hkdf;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.model.DecryptRequest;
import com.amazonaws.services.kms.model.DecryptResult;
import com.amazonaws.services.kms.model.GenerateDataKeyRequest;
import com.amazonaws.services.kms.model.GenerateDataKeyResult;
import com.amazonaws.util.Base64;
import com.amazonaws.util.StringUtils;
import com.amazonaws.util.VersionInfoUtils;
/**
* Generates a unique data key for each record in DynamoDB and protects that key
* using {@link AWSKMS}. Currently, the HashKey, RangeKey, and TableName will be
* included in the KMS EncryptionContext for wrapping/unwrapping the key. This
* means that records cannot be copied/moved between tables without re-encryption.
*
* @see KMS Encryption Context
*/
public class DirectKmsMaterialProvider implements EncryptionMaterialsProvider {
private static final String VERSION_STRING = "1.0";
private static final String USER_AGENT = DirectKmsMaterialProvider.class.getName()
+ "/" + VERSION_STRING + "/" + VersionInfoUtils.getVersion();
private static final String COVERED_ATTR_CTX_KEY = "aws-kms-ec-attr";
private static final String SIGNING_KEY_ALGORITHM = "amzn-ddb-sig-alg";
private static final String TABLE_NAME_EC_KEY = "*aws-kms-table*";
private static final String DEFAULT_ENC_ALG = "AES/256";
private static final String DEFAULT_SIG_ALG = "HmacSHA256/256";
private static final String KEY_COVERAGE = "*keys*";
private static final String KDF_ALG = "HmacSHA256";
private static final String KDF_SIG_INFO = "Signing";
private static final String KDF_ENC_INFO = "Encryption";
private final AWSKMS kms;
private final String encryptionKeyId;
private final Map description;
private final String dataKeyAlg;
private final int dataKeyLength;
private final String dataKeyDesc;
private final String sigKeyAlg;
private final int sigKeyLength;
private final String sigKeyDesc;
public DirectKmsMaterialProvider(AWSKMS kms) {
this(kms, null);
}
public DirectKmsMaterialProvider(AWSKMS kms, String encryptionKeyId, Map materialDescription) {
this.kms = kms;
this.encryptionKeyId = encryptionKeyId;
this.description = materialDescription != null ?
Collections.unmodifiableMap(new HashMap<>(materialDescription)) :
Collections. emptyMap();
dataKeyDesc = description
.containsKey(WrappedRawMaterials.CONTENT_KEY_ALGORITHM) ? description
.get(WrappedRawMaterials.CONTENT_KEY_ALGORITHM) : DEFAULT_ENC_ALG;
String[] parts = dataKeyDesc.split("/", 2);
this.dataKeyAlg = parts[0];
this.dataKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256;
sigKeyDesc = description
.containsKey(SIGNING_KEY_ALGORITHM) ? description
.get(SIGNING_KEY_ALGORITHM) : DEFAULT_SIG_ALG;
parts = sigKeyDesc.split("/", 2);
this.sigKeyAlg = parts[0];
this.sigKeyLength = parts.length == 2 ? Integer.parseInt(parts[1]) : 256;
}
public DirectKmsMaterialProvider(AWSKMS kms, String encryptionKeyId) {
this(kms, encryptionKeyId, Collections. emptyMap());
}
@Override
public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) {
final Map materialDescription = context.getMaterialDescription();
final Map ec = new HashMap<>();
final String providedEncAlg = materialDescription.get(CONTENT_KEY_ALGORITHM);
final String providedSigAlg = materialDescription.get(SIGNING_KEY_ALGORITHM);
ec.put("*" + CONTENT_KEY_ALGORITHM + "*", providedEncAlg);
ec.put("*" + SIGNING_KEY_ALGORITHM + "*", providedSigAlg);
populateKmsEcFromEc(context, ec);
DecryptRequest request = appendUserAgent(new DecryptRequest());
request.setCiphertextBlob(ByteBuffer.wrap(Base64.decode(materialDescription.get(ENVELOPE_KEY))));
request.setEncryptionContext(ec);
final DecryptResult decryptResult = kms.decrypt(request);
validateEncryptionKeyId(decryptResult.getKeyId(), context);
final Hkdf kdf;
try {
kdf = Hkdf.getInstance(KDF_ALG);
} catch (NoSuchAlgorithmException e) {
throw new DynamoDBMappingException(e);
}
kdf.init(toArray(decryptResult.getPlaintext()));
final String[] encAlgParts = providedEncAlg.split("/", 2);
int encLength = encAlgParts.length == 2 ? Integer.parseInt(encAlgParts[1]) : 256;
final String[] sigAlgParts = providedSigAlg.split("/", 2);
int sigLength = sigAlgParts.length == 2 ? Integer.parseInt(sigAlgParts[1]) : 256;
final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, encLength / 8), encAlgParts[0]);
final SecretKey macKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigLength / 8), sigAlgParts[0]);
return new SymmetricRawMaterials(encryptionKey, macKey, materialDescription);
}
@Override
public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
final Map ec = new HashMap<>();
ec.put("*" + CONTENT_KEY_ALGORITHM + "*", dataKeyDesc);
ec.put("*" + SIGNING_KEY_ALGORITHM + "*", sigKeyDesc);
populateKmsEcFromEc(context, ec);
final String keyId = selectEncryptionKeyId(context);
if (StringUtils.isNullOrEmpty(keyId)) {
throw new DynamoDBMappingException("Encryption key id is empty.");
}
final GenerateDataKeyRequest req = appendUserAgent(new GenerateDataKeyRequest());
req.setKeyId(keyId);
// NumberOfBytes parameter is used because we're not using this key as an AES-256 key,
// we're using it as an HKDF-SHA256 key.
req.setNumberOfBytes(256 / 8);
req.setEncryptionContext(ec);
final GenerateDataKeyResult dataKeyResult = kms.generateDataKey(req);
final Map materialDescription = new HashMap<>();
materialDescription.putAll(description);
materialDescription.put(COVERED_ATTR_CTX_KEY, KEY_COVERAGE);
materialDescription.put(KEY_WRAPPING_ALGORITHM, "kms");
materialDescription.put(CONTENT_KEY_ALGORITHM, dataKeyDesc);
materialDescription.put(SIGNING_KEY_ALGORITHM, sigKeyDesc);
materialDescription.put(ENVELOPE_KEY, Base64.encodeAsString(toArray(dataKeyResult.getCiphertextBlob())));
final Hkdf kdf;
try {
kdf = Hkdf.getInstance(KDF_ALG);
} catch (NoSuchAlgorithmException e) {
throw new DynamoDBMappingException(e);
}
kdf.init(toArray(dataKeyResult.getPlaintext()));
final SecretKey encryptionKey = new SecretKeySpec(kdf.deriveKey(KDF_ENC_INFO, dataKeyLength / 8), dataKeyAlg);
final SecretKey signatureKey = new SecretKeySpec(kdf.deriveKey(KDF_SIG_INFO, sigKeyLength / 8), sigKeyAlg);
return new SymmetricRawMaterials(encryptionKey, signatureKey, materialDescription);
}
/**
* Get encryption key id.
* @return encryption key id.
*/
protected String getEncryptionKeyId() {
return this.encryptionKeyId;
}
/**
* Select encryption key id to be used to generate data key. The default implementation of this method returns
* {@link DirectKmsMaterialProvider#encryptionKeyId}.
* @param context encryption context.
* @return the encryptionKeyId.
* @throws DynamoDBMappingException when we fails to select a valid encryption key id.
*/
protected String selectEncryptionKeyId(EncryptionContext context) throws DynamoDBMappingException {
return getEncryptionKeyId();
}
/**
* Validate the encryption key id. The default implementation of this method does nothing.
* @param encryptionKeyId encryption key id from {@link DecryptResult}.
* @param context encryption context.
* @throws DynamoDBMappingException when encryptionKeyId is invalid.
*/
protected void validateEncryptionKeyId(String encryptionKeyId, EncryptionContext context)
throws DynamoDBMappingException {
// No action taken.
}
/**
* Extracts relevant information from {@code context} and uses it to populate fields in
* {@code kmsEc}. Currently, these fields are:
*
* - {@code HashKeyName}
* - {@code HashKeyValue}
* - {@code RangeKeyName}
* - {@code RangeKeyValue}
* - {@link #TABLE_NAME_EC_KEY}
* - {@code TableName}
*/
private static void populateKmsEcFromEc(EncryptionContext context, Map kmsEc) {
final String hashKeyName = context.getHashKeyName();
if (hashKeyName != null) {
final AttributeValue hashKey = context.getAttributeValues().get(hashKeyName);
if (hashKey.getN() != null) {
kmsEc.put(hashKeyName, hashKey.getN());
} else if (hashKey.getS() != null) {
kmsEc.put(hashKeyName, hashKey.getS());
} else if (hashKey.getB() != null) {
kmsEc.put(hashKeyName, Base64.encodeAsString(toArray(hashKey.getB())));
} else {
throw new UnsupportedOperationException("DirectKmsMaterialProvider only supports String, Number, and Binary HashKeys");
}
}
final String rangeKeyName = context.getRangeKeyName();
if (rangeKeyName != null) {
final AttributeValue rangeKey = context.getAttributeValues().get(rangeKeyName);
if (rangeKey.getN() != null) {
kmsEc.put(rangeKeyName, rangeKey.getN());
} else if (rangeKey.getS() != null) {
kmsEc.put(rangeKeyName, rangeKey.getS());
} else if (rangeKey.getB() != null) {
kmsEc.put(rangeKeyName, Base64.encodeAsString(toArray(rangeKey.getB())));
} else {
throw new UnsupportedOperationException("DirectKmsMaterialProvider only supports String, Number, and Binary RangeKeys");
}
}
final String tableName = context.getTableName();
if (tableName != null) {
kmsEc.put(TABLE_NAME_EC_KEY, tableName);
}
}
private static byte[] toArray(final ByteBuffer buff) {
final ByteBuffer dup = buff.asReadOnlyBuffer();
byte[] result = new byte[dup.remaining()];
dup.get(result);
return result;
}
private static X appendUserAgent(final X request) {
request.getRequestClientOptions().appendUserAgent(USER_AGENT);
return request;
}
@Override
public void refresh() {
// No action needed
}
}