Skip to content

Update documentation and warnings related to SaveBehavior. #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The **[Amazon DynamoDB][ddb] Client-side Encryption in Java** supports encryptio

A typical use of this library is when you are using [DynamoDBMapper][ddbmapper], where transparent protection of all objects serialized through the mapper can be enabled via configuring an [AttributeEncryptor][attrencryptor].

**Important: Use `SaveBehavior.CLOBBER` with `AttributeEncryptor`. If you do not do so you risk corrupting your signatures and encrypted data.**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why this is the case?

Copy link
Contributor

@bdonlan bdonlan Aug 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Use SaveBehavior.CLOBBER with AttributeEncryptor. If you do not do so you risk corrupting your signatures and encrypted data. When CLOBBER is not specified, fields that are present in the record may not be passed down to the encryptor, which results in fields being left out of the record signature. This in turn can result in records failing to decrypt.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 - I want to learn more about why this is the desired behavior

Using CLOBBER basically disables the Optimistic Locking which is a super useful feature provided by DynamoDB. This says we shouldn't use AttributeEncryptor and Optimistic Locking together.

We support partial encryption and signing today, can those fields be updated safely without CLOBBER save?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will re-visit how optimistic logging works with encrypted data. It definitely will not work without a version field. This change is still important due to the need to warn people about the risk of data-corruption.

When CLOBBER is not specified, fields that are present in the record may not be passed down to the encryptor, which results in fields being left out of the record signature. This in turn can result in records failing to decrypt.

For more advanced use cases where tighter control over the encryption and signing process is necessary, the low-level [DynamoDBEncryptor][ddbencryptor] can be used directly.

## Getting Started
Expand Down Expand Up @@ -74,7 +77,7 @@ To enable transparent encryption and signing, simply specify the necessary encry
SecretKey cek = ...; // Content encrypting key
SecretKey macKey = ...; // Signing key
EncryptionMaterialsProvider provider = new SymmetricStaticProvider(cek, macKey);
mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT,
mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build(),
new AttributeEncryptor(provider));
Book book = new Book();
book.setId(123);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static void encryptRecord(final String cmkArn, final String region) {
// Encryptor creation
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
// Mapper Creation
// Please note the use of SaveBehavior.CLOBBER. Omitting this can result in data-corruption.
DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build();
DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand All @@ -32,13 +33,18 @@
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#CLOBBER}. Use of
* any other @{code SaveBehavior} can result in data-corruption.</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<>();

Expand All @@ -58,9 +64,26 @@ public DynamoDBEncryptor getEncryptor() {
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a short comment explaining the case being handled here / why this is set to behave this way, for clarity/maintainers?

// 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. " +
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anyone who is not using CLOBBER today, there will be tons of errors in the log and the operation is not actually failing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct because they are in a dangerous error case and are at risk for data-corruption. Minor software changes outside of their (or our) control could result in this going from a "working due to luck" state to a "broken" state. We consider it sufficiently important to notify people of this dangerous configuration that an ERROR level log is appropriate.

The next minor (non-patch) version will throw an exception in this case. Please see #32

"This occured while trying to save " + parameters.getModelClass());
}

try {
return encryptor.encryptRecord(
parameters.getAttributeValues(),
attributeValues,
metadata.getEncryptionFlags(),
paramsToContext(parameters));
} catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ public class TransformerHolisticTests {
private static final EncryptionMaterialsProvider symWrappedProv;

private AmazonDynamoDB client;
// AttributeEncryptor *must* be used with SaveBehavior.CLOBBER to avoid the risk of data corruption.
private static final DynamoDBMapperConfig CLOBBER_CONFIG =
DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build();
private static final BaseClass ENCRYPTED_TEST_VALUE = new BaseClass();
private static final Mixed MIXED_TEST_VALUE = new Mixed();
private static final SignOnly SIGNED_TEST_VALUE = new SignOnly();
Expand Down Expand Up @@ -292,7 +295,7 @@ public void setUp() {

@Test
public void simpleSaveLoad() {
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(symProv));
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(symProv));
Mixed obj = new Mixed();
obj.setHashKey(0);
obj.setRangeKey(15);
Expand Down Expand Up @@ -322,7 +325,7 @@ public void simpleSaveLoad() {

@Test
public void leadingAndTrailingZeros() {
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(symProv));
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(symProv));
Mixed obj = new Mixed();
obj.setHashKey(0);
obj.setRangeKey(15);
Expand Down Expand Up @@ -364,7 +367,7 @@ public void leadingAndTrailingZeros() {

@Test
public void simpleSaveLoadAsym() {
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(asymProv));
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(asymProv));

BaseClass obj = new BaseClass();
obj.setHashKey(0);
Expand Down Expand Up @@ -394,7 +397,7 @@ public void simpleSaveLoadAsym() {

@Test
public void simpleSaveLoadHashOnly() {
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(
symProv));

HashKeyOnly obj = new HashKeyOnly("");
Expand All @@ -411,7 +414,7 @@ public void simpleSaveLoadHashOnly() {

@Test
public void simpleSaveLoadKeysOnly() {
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(
asymProv));

KeysOnly obj = new KeysOnly();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@
import com.amazonaws.services.dynamodbv2.model.AttributeValue;

public class FakeParameters<T> {
public static <T> AttributeTransformer.Parameters<T> getInstance(Class<T> clazz,
Map<String, AttributeValue> attribs, DynamoDBMapperConfig config, String tableName,
String hashKeyName, String rangeKeyName) {
return getInstance(clazz, attribs, config, tableName, hashKeyName, rangeKeyName, false);
}

public static <T> AttributeTransformer.Parameters<T> getInstance(Class<T> clazz,
Map<String, AttributeValue> attribs, DynamoDBMapperConfig config, String tableName,
String hashKeyName, String rangeKeyName) {
String hashKeyName, String rangeKeyName, boolean isPartialUpdate) {

// We use this relatively insane proxy setup so that modifications to the Parameters
// interface doesn't break our tests (unless it actually impacts our code).
FakeParameters<T> fakeParams = new FakeParameters<T>(clazz, attribs, config, tableName,
hashKeyName, rangeKeyName);
hashKeyName, rangeKeyName, isPartialUpdate);
@SuppressWarnings("unchecked")
AttributeTransformer.Parameters<T> proxyObject = (AttributeTransformer.Parameters<T>) Proxy
.newProxyInstance(AttributeTransformer.class.getClassLoader(),
Expand Down Expand Up @@ -65,16 +71,19 @@ public Object invoke(Object obj, Method method, Object[] args) throws Throwable
private final String tableName;
private final String hashKeyName;
private final String rangeKeyName;
private final boolean isPartialUpdate;

private FakeParameters(Class<T> clazz, Map<String, AttributeValue> attribs,
DynamoDBMapperConfig config, String tableName, String hashKeyName, String rangeKeyName) {
DynamoDBMapperConfig config, String tableName, String hashKeyName, String rangeKeyName,
boolean isPartialUpdate) {
super();
this.clazz = clazz;
this.attrs = Collections.unmodifiableMap(attribs);
this.config = config;
this.tableName = tableName;
this.hashKeyName = hashKeyName;
this.rangeKeyName = rangeKeyName;
this.isPartialUpdate = isPartialUpdate;
}

public Map<String, AttributeValue> getAttributeValues() {
Expand All @@ -100,4 +109,8 @@ public String getHashKeyName() {
public String getRangeKeyName() {
return rangeKeyName;
}

public boolean isPartialUpdate() {
return isPartialUpdate;
}
}