com.amazonaws.services.dynamodbv2.datamodeling.AttributeEncryptor.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.services.dynamodbv2.datamodeling.AttributeEncryptor.java

Source

/*
 * 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.
 * <em>This must be used with @{link SaveBehavior#PUT} or @{link SaveBehavior#CLOBBER}.</em>
 * 
 * @author Greg Rubin 
 */
public class AttributeEncryptor implements AttributeTransformer {
    private static final Log LOG = LogFactory.getLog(AttributeEncryptor.class);
    private final DynamoDBEncryptor encryptor;
    private final Map<Class<?>, 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<String, AttributeValue> transform(final Parameters<?> parameters) {
        // one map of attributeFlags per model class
        final ModelClassMetadata metadata = getModelClassMetadata(parameters);

        final Map<String, AttributeValue> 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.PUT or 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()) {
            throw new DynamoDBMappingException(
                    "Use of AttributeEncryptor without SaveBehavior.PUT or 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<String, AttributeValue> untransform(final Parameters<?> parameters) {
        final Map<String, Set<EncryptionFlags>> 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<String, Set<EncryptionFlags>> 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<EncryptionFlags> unknownAttributeBehavior = metadata.getUnknownAttributeBehavior();
        final Map<String, Set<EncryptionFlags>> 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 <T> ModelClassMetadata getModelClassMetadata(Parameters<T> 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<T> clazz = parameters.getModelClass();
        ModelClassMetadata metadata = metadataCache.get(clazz);

        if (metadata == null) {
            Map<String, Set<EncryptionFlags>> attributeFlags = new HashMap<>();

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

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

                for (Mapping mapping : mappings.getMappings()) {
                    final EnumSet<EncryptionFlags> 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<String, Set<EncryptionFlags>> encryptionFlags;
        private final boolean doNotTouch;
        private final Set<EncryptionFlags> unknownAttributeBehavior;

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

        public Map<String, Set<EncryptionFlags>> getEncryptionFlags() {
            return encryptionFlags;
        }

        public boolean getDoNotTouch() {
            return doNotTouch;
        }

        public Set<EncryptionFlags> getUnknownAttributeBehavior() {
            return unknownAttributeBehavior;
        }
    }
}