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

com.amazonaws.services.dynamodbv2.datamodeling.AttributeEncryptor 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;

import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingsRegistry.Mapping;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingsRegistry.Mappings;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DoNotEncrypt;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DoNotTouch;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionFlags;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.HandleUnknownAttributes;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.TableAadOverride;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.EncryptionMaterialsProvider;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Encrypts all non-key fields prior to storing them in DynamoDB.
 * This must be used with @{link SaveBehavior#CLOBBER}. Use of
 * any other @{code SaveBehavior} can result in data-corruption.
 * 
 * @author Greg Rubin 
 */
public class AttributeEncryptor implements AttributeTransformer {
    private static final Log LOG = LogFactory.getLog(AttributeEncryptor.class);
    private final DynamoDBEncryptor encryptor;
    private final Map, ModelClassMetadata> metadataCache = new ConcurrentHashMap<>();

    public AttributeEncryptor(final DynamoDBEncryptor encryptor) {
        this.encryptor = encryptor;
    }

    public AttributeEncryptor(final EncryptionMaterialsProvider encryptionMaterialsProvider) {
        encryptor = DynamoDBEncryptor.getInstance(encryptionMaterialsProvider);
    }

    public DynamoDBEncryptor getEncryptor() {
        return encryptor;
    }

    @Override
    public Map transform(final Parameters parameters) {
        // one map of attributeFlags per model class
        final ModelClassMetadata metadata = getModelClassMetadata(parameters);

        final Map attributeValues = parameters.getAttributeValues();
        // If this class is marked as "DoNotTouch" then we know our encryptor will not change it at all
        // so we may as well fast-return and do nothing. This also avoids emitting errors when they would not apply.
        if (metadata.doNotTouch) {
            return attributeValues;
        }

        // When AttributeEncryptor is used without SaveBehavior.CLOBBER, it is trying to transform only a subset
        // of the actual fields stored in DynamoDB. This means that the generated signature will not cover any
        // unmodified fields. Thus, upon untransform, the signature verification will fail as it won't cover all
        // expected fields.
        if (parameters.isPartialUpdate()) {
            LOG.error("Use of AttributeEncryptor without SaveBehavior.CLOBBER is an error and can result in data-corruption. " +
                    "This occured while trying to save " + parameters.getModelClass());
        }

        try {
            return encryptor.encryptRecord(
                    attributeValues,
                    metadata.getEncryptionFlags(),
                    paramsToContext(parameters));
        } catch (Exception ex) {
            throw new DynamoDBMappingException(ex);
        }
    }

    @Override
    public Map untransform(final Parameters parameters) {
        final Map> attributeFlags = getEncryptionFlags(parameters);

        try {
            return encryptor.decryptRecord(
                    parameters.getAttributeValues(),
                    attributeFlags,
                    paramsToContext(parameters));
        } catch (Exception ex) {
            throw new DynamoDBMappingException(ex);
        }
    }

    /*
     * For any attributes we see from DynamoDB that aren't modeled in the mapper class,
     * we either ignore them (the default behavior), or include them for encryption/signing
     * based on the presence of the @HandleUnknownAttributes annotation (unless the class
     * has @DoNotTouch, then we don't include them).
     */
    private Map> getEncryptionFlags(final Parameters parameters) {
        final ModelClassMetadata metadata = getModelClassMetadata(parameters);
        
        // If the class is annotated with @DoNotTouch, then none of the attributes are
        // encrypted or signed, so we don't need to bother looking for unknown attributes.
        if (metadata.getDoNotTouch()) {
            return metadata.getEncryptionFlags();
        }

        final Set unknownAttributeBehavior = metadata.getUnknownAttributeBehavior();
        final Map> attributeFlags = new HashMap<>();
        attributeFlags.putAll(metadata.getEncryptionFlags());
        
        for (final String attributeName : parameters.getAttributeValues().keySet()) {
            if (!attributeFlags.containsKey(attributeName) && 
                    !encryptor.getSignatureFieldName().equals(attributeName) &&
                    !encryptor.getMaterialDescriptionFieldName().equals(attributeName)) {

                attributeFlags.put(attributeName, unknownAttributeBehavior);
            }
        }
        
        return attributeFlags;
    }

    private  ModelClassMetadata getModelClassMetadata(Parameters parameters) {
        // Due to the lack of explicit synchronization, it is possible that
        // elements in the cache will be added multiple times. Since they will
        // all be identical, this is okay. Avoiding explicit synchronization
        // means that in the general (retrieval) case, should never block and
        // should be extremely fast.
        final Class clazz = parameters.getModelClass();
        ModelClassMetadata metadata = metadataCache.get(clazz);

        if (metadata == null) {
            Map> attributeFlags = new HashMap<>();

            final boolean handleUnknownAttributes = handleUnknownAttributes(clazz);
            final EnumSet unknownAttributeBehavior = EnumSet.noneOf(EncryptionFlags.class);

            if (shouldTouch(clazz)) {
                Mappings mappings = DynamoDBMappingsRegistry.instance().mappingsOf(clazz);

                for (Mapping mapping : mappings.getMappings()) {
                    final EnumSet flags = EnumSet.noneOf(EncryptionFlags.class);
                    if (shouldTouch(mapping)) {
                        if (shouldEncryptAttribute(clazz, mapping)) {
                            flags.add(EncryptionFlags.ENCRYPT);
                        }
                        flags.add(EncryptionFlags.SIGN);
                    }
                    attributeFlags.put(mapping.getAttributeName(), Collections.unmodifiableSet(flags));
                }

                if (handleUnknownAttributes) {
                    unknownAttributeBehavior.add(EncryptionFlags.SIGN);

                    if (shouldEncrypt(clazz)) {
                        unknownAttributeBehavior.add(EncryptionFlags.ENCRYPT);
                    }
                }
            }

            metadata = new ModelClassMetadata(Collections.unmodifiableMap(attributeFlags), doNotTouch(clazz),
                    Collections.unmodifiableSet(unknownAttributeBehavior));
            metadataCache.put(clazz, metadata);
        }
        return metadata;
    }

    /**
     * @return True if {@link DoNotTouch} is not present on the class level. False otherwise
     */
    private boolean shouldTouch(Class clazz) {
        return !doNotTouch(clazz);
    }

    /**
     * @return True if {@link DoNotTouch} is not present on the getter level. False otherwise.
     */
    private boolean shouldTouch(Mapping mapping) {
        return !doNotTouch(mapping);
    }

    /**
     * @return True if {@link DoNotTouch} IS present on the class level. False otherwise.
     */
    private boolean doNotTouch(Class clazz) {
        return clazz.isAnnotationPresent(DoNotTouch.class);
    }

    /**
     * @return True if {@link DoNotTouch} IS present on the getter level. False otherwise.
     */
    private boolean doNotTouch(Mapping mapping) {
        return mapping.getter().isAnnotationPresent(DoNotTouch.class);
    }

    /**
     * @return True if {@link DoNotEncrypt} is NOT present on the class level. False otherwise.
     */
    private boolean shouldEncrypt(Class clazz) {
        return !doNotEncrypt(clazz);
    }

    /**
     * @return True if {@link DoNotEncrypt} IS present on the class level. False otherwise.
     */
    private boolean doNotEncrypt(Class clazz) {
        return clazz.isAnnotationPresent(DoNotEncrypt.class);
    }

    /**
     * @return True if {@link DoNotEncrypt} IS present on the getter level. False otherwise.
     */
    private boolean doNotEncrypt(Mapping mapping) {
        return mapping.getter().isAnnotationPresent(DoNotEncrypt.class);
    }

    /**
     * @return True if the attribute should be encrypted, false otherwise.
     */
    private boolean shouldEncryptAttribute(final Class clazz, final Mapping mapping) {
        return !(doNotEncrypt(clazz) || doNotEncrypt(mapping) || mapping.isPrimaryKey() || mapping.isVersion());
    }

    private static EncryptionContext paramsToContext(Parameters params) {
        final Class clazz = params.getModelClass();
        final TableAadOverride override = clazz.getAnnotation(TableAadOverride.class);
        final String tableName = ((override == null) ? params.getTableName() : override.tableName());

        return new EncryptionContext.Builder()
                .withHashKeyName(params.getHashKeyName())
                .withRangeKeyName(params.getRangeKeyName())
                .withTableName(tableName)
                .withModeledClass(params.getModelClass())
                .withAttributeValues(params.getAttributeValues()).build();
    }

    private boolean handleUnknownAttributes(Class clazz) {
        return clazz.getAnnotation(HandleUnknownAttributes.class) != null;
    }

    private static class ModelClassMetadata {
        private final Map> encryptionFlags;
        private final boolean doNotTouch;
        private final Set unknownAttributeBehavior;

        public ModelClassMetadata(Map> encryptionFlags, 
                boolean doNotTouch, Set unknownAttributeBehavior) {
            this.encryptionFlags = encryptionFlags;
            this.doNotTouch = doNotTouch;
            this.unknownAttributeBehavior = unknownAttributeBehavior;
        }

        public Map> getEncryptionFlags() {
            return encryptionFlags;
        }

        public boolean getDoNotTouch() {
            return doNotTouch;
        }

        public Set getUnknownAttributeBehavior() {
            return unknownAttributeBehavior;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy