All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor Maven / Gradle / Ivy

There is a newer version: 2.0.3
Show newest version
/*
 * 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