com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor 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;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import com.amazonaws.services.dynamodbv2.datamodeling.AttributeEncryptor;
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.providers.EncryptionMaterialsProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.internal.AttributeValueMarshaller;
import com.amazonaws.services.dynamodbv2.datamodeling.internal.ByteBufferInputStream;
import com.amazonaws.services.dynamodbv2.datamodeling.internal.Utils;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
/**
* The low-level API used by {@link AttributeEncryptor} to perform crypto
* operations on the record attributes.
*
* @author Greg Rubin
*/
public class DynamoDBEncryptor {
private static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final String DEFAULT_METADATA_FIELD = "*amzn-ddb-map-desc*";
private static final String DEFAULT_SIGNATURE_FIELD = "*amzn-ddb-map-sig*";
private static final String DEFAULT_DESCRIPTION_BASE = "amzn-ddb-map-"; // Same as the Mapper
private static final Charset UTF8 = Charset.forName("UTF-8");
private static final String SYMMETRIC_ENCRYPTION_MODE = "/CBC/PKCS5Padding";
private static final int CURRENT_VERSION = 0;
private String signatureFieldName = DEFAULT_SIGNATURE_FIELD;
private String materialDescriptionFieldName = DEFAULT_METADATA_FIELD;
private EncryptionMaterialsProvider encryptionMaterialsProvider;
private final String descriptionBase;
private final String symmetricEncryptionModeHeader;
private final String signingAlgorithmHeader;
public static final String DEFAULT_SIGNING_ALGORITHM_HEADER = DEFAULT_DESCRIPTION_BASE + "signingAlg";
protected DynamoDBEncryptor(EncryptionMaterialsProvider provider, String descriptionBase) {
this.encryptionMaterialsProvider = provider;
this.descriptionBase = descriptionBase;
symmetricEncryptionModeHeader = this.descriptionBase + "sym-mode";
signingAlgorithmHeader = this.descriptionBase + "signingAlg";
}
public static DynamoDBEncryptor getInstance(EncryptionMaterialsProvider provider, String descriptionbase) {
return new DynamoDBEncryptor(provider, descriptionbase);
}
public static DynamoDBEncryptor getInstance(EncryptionMaterialsProvider provider) {
return getInstance(provider, DEFAULT_DESCRIPTION_BASE);
}
/**
* Returns a decrypted version of the provided DynamoDb record. The signature is verified across
* all provided fields. All fields (except those listed in doNotEncrypt
are
* decrypted.
*
* @param itemAttributes
* the DynamoDbRecord
* @param context
* additional information used to successfully select the encryption materials and
* decrypt the data. This should include (at least) the tableName and the
* materialDescription.
* @param doNotDecrypt
* those fields which should not be encrypted
* @return a plaintext version of the DynamoDb record
* @throws SignatureException
* if the signature is invalid or cannot be verified
* @throws GeneralSecurityException
*/
public Map decryptAllFieldsExcept(Map itemAttributes,
EncryptionContext context, String... doNotDecrypt) throws GeneralSecurityException {
return decryptAllFieldsExcept(itemAttributes, context, Arrays.asList(doNotDecrypt));
}
/**
* @see #decryptAllFieldsExcept(Map, EncryptionContext, String...)
*/
public Map decryptAllFieldsExcept(
Map itemAttributes,
EncryptionContext context, Collection doNotDecrypt)
throws GeneralSecurityException {
Map> attributeFlags = allDecryptionFlagsExcept(
itemAttributes, doNotDecrypt);
return decryptRecord(itemAttributes, attributeFlags, context);
}
/**
* Returns the decryption flags for all item attributes except for those
* explicitly specified to be excluded.
* @param doNotDecrypt fields to be excluded
*/
public Map> allDecryptionFlagsExcept(
Map itemAttributes,
String ... doNotDecrypt) {
return allDecryptionFlagsExcept(itemAttributes, Arrays.asList(doNotDecrypt));
}
/**
* Returns the decryption flags for all item attributes except for those
* explicitly specified to be excluded.
* @param doNotDecrypt fields to be excluded
*/
public Map> allDecryptionFlagsExcept(
Map itemAttributes,
Collection doNotDecrypt) {
Map> attributeFlags = new HashMap>();
for (String fieldName : doNotDecrypt) {
attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.SIGN));
}
for (String fieldName : itemAttributes.keySet()) {
if (!attributeFlags.containsKey(fieldName) &&
!fieldName.equals(getMaterialDescriptionFieldName()) &&
!fieldName.equals(getSignatureFieldName())) {
attributeFlags.put(fieldName,
EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN));
}
}
return attributeFlags;
}
/**
* Returns an encrypted version of the provided DynamoDb record. All fields are signed. All fields
* (except those listed in doNotEncrypt
) are encrypted.
* @param itemAttributes a DynamoDb Record
* @param context
* additional information used to successfully select the encryption materials and
* encrypt the data. This should include (at least) the tableName.
* @param doNotEncrypt those fields which should not be encrypted
* @return a ciphertext version of the DynamoDb record
* @throws GeneralSecurityException
*/
public Map encryptAllFieldsExcept(Map itemAttributes,
EncryptionContext context, String... doNotEncrypt) throws GeneralSecurityException {
return encryptAllFieldsExcept(itemAttributes, context, Arrays.asList(doNotEncrypt));
}
public Map encryptAllFieldsExcept(
Map itemAttributes,
EncryptionContext context,
Collection doNotEncrypt)
throws GeneralSecurityException {
Map> attributeFlags = allEncryptionFlagsExcept(
itemAttributes, doNotEncrypt);
return encryptRecord(itemAttributes, attributeFlags, context);
}
/**
* Returns the encryption flags for all item attributes except for those
* explicitly specified to be excluded.
* @param doNotEncrypt fields to be excluded
*/
public Map> allEncryptionFlagsExcept(
Map itemAttributes,
String ...doNotEncrypt) {
return allEncryptionFlagsExcept(itemAttributes, Arrays.asList(doNotEncrypt));
}
/**
* Returns the encryption flags for all item attributes except for those
* explicitly specified to be excluded.
* @param doNotEncrypt fields to be excluded
*/
public Map> allEncryptionFlagsExcept(
Map itemAttributes,
Collection doNotEncrypt) {
Map> attributeFlags =
new HashMap>();
for (String fieldName : doNotEncrypt) {
attributeFlags.put(fieldName, EnumSet.of(EncryptionFlags.SIGN));
}
for (String fieldName : itemAttributes.keySet()) {
if (!attributeFlags.containsKey(fieldName)) {
attributeFlags.put(fieldName,
EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN));
}
}
return attributeFlags;
}
public Map decryptRecord(
Map itemAttributes,
Map> attributeFlags,
EncryptionContext context) throws GeneralSecurityException {
if (attributeFlags.isEmpty()) {
return itemAttributes;
}
// Copy to avoid changing anyone elses objects
itemAttributes = new HashMap(itemAttributes);
Map materialDescription = Collections.emptyMap();
DecryptionMaterials materials;
SecretKey decryptionKey;
DynamoDBSigner signer = DynamoDBSigner.getInstance(DEFAULT_SIGNATURE_ALGORITHM, Utils.getRng());
if (itemAttributes.containsKey(materialDescriptionFieldName)) {
materialDescription = unmarshallDescription(itemAttributes.get(materialDescriptionFieldName));
}
// Copy the material description and attribute values into the context
context = new EncryptionContext.Builder(context)
.withMaterialDescription(materialDescription)
.withAttributeValues(itemAttributes)
.build();
materials = encryptionMaterialsProvider.getDecryptionMaterials(context);
decryptionKey = materials.getDecryptionKey();
if (materialDescription.containsKey(signingAlgorithmHeader)) {
String signingAlg = materialDescription.get(signingAlgorithmHeader);
signer = DynamoDBSigner.getInstance(signingAlg, Utils.getRng());
}
ByteBuffer signature;
if (!itemAttributes.containsKey(signatureFieldName) || itemAttributes.get(signatureFieldName).getB() == null) {
signature = ByteBuffer.allocate(0);
} else {
signature = itemAttributes.get(signatureFieldName).getB().asReadOnlyBuffer();
}
itemAttributes.remove(signatureFieldName);
String associatedData = "TABLE>" + context.getTableName() + " encryptRecord(
Map itemAttributes,
Map> attributeFlags,
EncryptionContext context) throws GeneralSecurityException {
if (attributeFlags.isEmpty()) {
return itemAttributes;
}
// Copy to avoid changing anyone elses objects
itemAttributes = new HashMap(itemAttributes);
// Copy the attribute values into the context
context = new EncryptionContext.Builder(context)
.withAttributeValues(itemAttributes)
.build();
EncryptionMaterials materials = encryptionMaterialsProvider.getEncryptionMaterials(context);
// We need to copy this because we modify it to record other encryption details
Map materialDescription = new HashMap(
materials.getMaterialDescription());
SecretKey encryptionKey = materials.getEncryptionKey();
actualEncryption(itemAttributes, attributeFlags, materialDescription, encryptionKey);
// The description must be stored after encryption because its data
// is necessary for proper decryption.
final String signingAlgo = materialDescription.get(signingAlgorithmHeader);
DynamoDBSigner signer;
if (signingAlgo != null) {
signer = DynamoDBSigner.getInstance(signingAlgo, Utils.getRng());
} else {
signer = DynamoDBSigner.getInstance(DEFAULT_SIGNATURE_ALGORITHM, Utils.getRng());
}
if (materials.getSigningKey() instanceof PrivateKey ) {
materialDescription.put(signingAlgorithmHeader, signer.getSigningAlgorithm());
}
if (!materialDescription.isEmpty()) {
itemAttributes.put(materialDescriptionFieldName, marshallDescription(materialDescription));
}
String associatedData = "TABLE>" + context.getTableName() + " itemAttributes,
Map> attributeFlags, SecretKey encryptionKey,
Map materialDescription) throws GeneralSecurityException {
final String encryptionMode = encryptionKey != null ? encryptionKey.getAlgorithm() +
materialDescription.get(symmetricEncryptionModeHeader) : null;
Cipher cipher = null;
int ivSize = -1;
for (Map.Entry entry: itemAttributes.entrySet()) {
Set flags = attributeFlags.get(entry.getKey());
if (flags != null && flags.contains(EncryptionFlags.ENCRYPT)) {
if (!flags.contains(EncryptionFlags.SIGN)) {
throw new IllegalArgumentException("All encrypted fields must be signed. Bad field: " + entry.getKey());
}
ByteBuffer plainText;
ByteBuffer cipherText = entry.getValue().getB().asReadOnlyBuffer();
cipherText.rewind();
if (encryptionKey instanceof DelegatedKey) {
plainText = ByteBuffer.wrap(((DelegatedKey)encryptionKey).decrypt(toByteArray(cipherText), null, encryptionMode));
} else {
if (cipher == null) {
cipher = Cipher.getInstance(
encryptionMode);
ivSize = cipher.getBlockSize();
}
byte[] iv = new byte[ivSize];
cipherText.get(iv);
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(iv), Utils.getRng());
plainText = ByteBuffer.allocate(
cipher.getOutputSize(cipherText.remaining()));
cipher.doFinal(cipherText, plainText);
plainText.rewind();
}
entry.setValue(AttributeValueMarshaller.unmarshall(plainText));
}
}
}
/**
* This method has the side effect of replacing the plaintext
* attribute-values of "itemAttributes" with ciphertext attribute-values
* (which are always in the form of ByteBuffer) as per the corresponding
* attribute flags.
*/
private void actualEncryption(Map itemAttributes,
Map> attributeFlags,
Map materialDescription,
SecretKey encryptionKey) throws GeneralSecurityException {
String encryptionMode = null;
if (encryptionKey != null) {
materialDescription.put(this.symmetricEncryptionModeHeader,
SYMMETRIC_ENCRYPTION_MODE);
encryptionMode = encryptionKey.getAlgorithm() + SYMMETRIC_ENCRYPTION_MODE;
}
Cipher cipher = null;
int ivSize = -1;
for (Map.Entry entry: itemAttributes.entrySet()) {
Set flags = attributeFlags.get(entry.getKey());
if (flags != null && flags.contains(EncryptionFlags.ENCRYPT)) {
if (!flags.contains(EncryptionFlags.SIGN)) {
throw new IllegalArgumentException("All encrypted fields must be signed. Bad field: " + entry.getKey());
}
ByteBuffer plainText = AttributeValueMarshaller.marshall(entry.getValue());
plainText.rewind();
ByteBuffer cipherText;
if (encryptionKey instanceof DelegatedKey) {
DelegatedKey dk = (DelegatedKey) encryptionKey;
cipherText = ByteBuffer.wrap(
dk.encrypt(toByteArray(plainText), null, encryptionMode));
} else {
if (cipher == null) {
cipher = Cipher.getInstance(encryptionMode);
ivSize = cipher.getBlockSize();
}
// Encryption format:
// Note a unique iv is generated per attribute
byte[] iv = Utils.getRandom(ivSize);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv), Utils.getRng());
cipherText = ByteBuffer.allocate(ivSize + cipher.getOutputSize(plainText.remaining()));
cipherText.put(iv);
cipher.doFinal(plainText, cipherText);
cipherText.rewind();
}
// Replace the plaintext attribute value with the encrypted content
entry.setValue(new AttributeValue().withB(cipherText));
}
}
}
/**
* Get the name of the DynamoDB field used to store the signature.
* Defaults to {@link #DEFAULT_SIGNATURE_FIELD}.
*
* @return the name of the DynamoDB field used to store the signature
*/
public String getSignatureFieldName() {
return signatureFieldName;
}
/**
* Set the name of the DynamoDB field used to store the signature.
*
* @param signatureFieldName
*/
public void setSignatureFieldName(final String signatureFieldName) {
this.signatureFieldName = signatureFieldName;
}
/**
* Get the name of the DynamoDB field used to store metadata used by the
* DynamoDBEncryptedMapper. Defaults to {@link #DEFAULT_METADATA_FIELD}.
*
* @return the name of the DynamoDB field used to store metadata used by the
* DynamoDBEncryptedMapper
*/
public String getMaterialDescriptionFieldName() {
return materialDescriptionFieldName;
}
/**
* Set the name of the DynamoDB field used to store metadata used by the
* DynamoDBEncryptedMapper
*
* @param materialDescriptionFieldName
*/
public void setMaterialDescriptionFieldName(final String materialDescriptionFieldName) {
this.materialDescriptionFieldName = materialDescriptionFieldName;
}
/**
* Marshalls the description
into a ByteBuffer by outputting
* each key (modified UTF-8) followed by its value (also in modified UTF-8).
*
* @param description
* @return the description encoded as an AttributeValue with a ByteBuffer value
* @see java.io.DataOutput#writeUTF(String)
*/
protected static AttributeValue marshallDescription(Map description) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
out.writeInt(CURRENT_VERSION);
for (Map.Entry entry : description.entrySet()) {
byte[] bytes = entry.getKey().getBytes(UTF8);
out.writeInt(bytes.length);
out.write(bytes);
bytes = entry.getValue().getBytes(UTF8);
out.writeInt(bytes.length);
out.write(bytes);
}
out.close();
AttributeValue result = new AttributeValue();
result.setB(ByteBuffer.wrap(bos.toByteArray()));
return result;
} catch (IOException ex) {
// Due to the objects in use, an IOException is not possible.
throw new RuntimeException("Unexpected exception", ex);
}
}
public String getSigningAlgorithmHeader() {
return signingAlgorithmHeader;
}
/**
* @see #marshallDescription(Map)
*/
protected static Map unmarshallDescription(AttributeValue attributeValue) {
attributeValue.getB().mark();
try (DataInputStream in = new DataInputStream(
new ByteBufferInputStream(attributeValue.getB())) ) {
Map result = new HashMap();
int version = in.readInt();
if (version != CURRENT_VERSION) {
throw new IllegalArgumentException("Unsupported description version");
}
String key, value;
int keyLength, valueLength;
try {
while(in.available() > 0) {
keyLength = in.readInt();
byte[] bytes = new byte[keyLength];
if (in.read(bytes) != keyLength) {
throw new IllegalArgumentException("Malformed description");
}
key = new String(bytes, UTF8);
valueLength = in.readInt();
bytes = new byte[valueLength];
if (in.read(bytes) != valueLength) {
throw new IllegalArgumentException("Malformed description");
}
value = new String(bytes, UTF8);
result.put(key, value);
}
} catch (EOFException eof) {
throw new IllegalArgumentException("Malformed description", eof);
}
return result;
} catch (IOException ex) {
// Due to the objects in use, an IOException is not possible.
throw new RuntimeException("Unexpected exception", ex);
} finally {
attributeValue.getB().reset();
}
}
private static byte[] toByteArray(ByteBuffer buffer) {
if (buffer.hasArray()) {
byte[] result = buffer.array();
buffer.rewind();
return result;
} else {
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
buffer.rewind();
return result;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy