Skip to content

Commit 4d6b40e

Browse files
authored
Merge pull request #44 from SalusaSecondus/issue-42
Update documentation and warnings related to SaveBehavior.
2 parents ded8364 + 7530a1e commit 4d6b40e

File tree

5 files changed

+53
-10
lines changed

5 files changed

+53
-10
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ The **[Amazon DynamoDB][ddb] Client-side Encryption in Java** supports encryptio
44

55
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].
66

7+
**Important: Use `SaveBehavior.CLOBBER` with `AttributeEncryptor`. If you do not do so you risk corrupting your signatures and encrypted data.**
8+
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.
9+
710
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.
811

912
## Getting Started
@@ -74,7 +77,7 @@ To enable transparent encryption and signing, simply specify the necessary encry
7477
SecretKey cek = ...; // Content encrypting key
7578
SecretKey macKey = ...; // Signing key
7679
EncryptionMaterialsProvider provider = new SymmetricStaticProvider(cek, macKey);
77-
mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT,
80+
mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build(),
7881
new AttributeEncryptor(provider));
7982
Book book = new Book();
8083
book.setId(123);

examples/com/amazonaws/examples/AwsKmsEncryptedObject.java

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public static void encryptRecord(final String cmkArn, final String region) {
5252
// Encryptor creation
5353
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
5454
// Mapper Creation
55+
// Please note the use of SaveBehavior.CLOBBER. Omitting this can result in data-corruption.
5556
DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build();
5657
DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor));
5758

src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/AttributeEncryptor.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Set;
2222
import java.util.concurrent.ConcurrentHashMap;
2323

24+
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior;
2425
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingsRegistry.Mapping;
2526
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingsRegistry.Mappings;
2627
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DoNotEncrypt;
@@ -32,13 +33,18 @@
3233
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.TableAadOverride;
3334
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.EncryptionMaterialsProvider;
3435
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
36+
import org.apache.commons.logging.Log;
37+
import org.apache.commons.logging.LogFactory;
3538

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

@@ -58,9 +64,26 @@ public DynamoDBEncryptor getEncryptor() {
5864
public Map<String, AttributeValue> transform(final Parameters<?> parameters) {
5965
// one map of attributeFlags per model class
6066
final ModelClassMetadata metadata = getModelClassMetadata(parameters);
67+
68+
final Map<String, AttributeValue> attributeValues = parameters.getAttributeValues();
69+
// If this class is marked as "DoNotTouch" then we know our encryptor will not change it at all
70+
// so we may as well fast-return and do nothing. This also avoids emitting errors when they would not apply.
71+
if (metadata.doNotTouch) {
72+
return attributeValues;
73+
}
74+
75+
// When AttributeEncryptor is used without SaveBehavior.CLOBBER, it is trying to transform only a subset
76+
// of the actual fields stored in DynamoDB. This means that the generated signature will not cover any
77+
// unmodified fields. Thus, upon untransform, the signature verification will fail as it won't cover all
78+
// expected fields.
79+
if (parameters.isPartialUpdate()) {
80+
LOG.error("Use of AttributeEncryptor without SaveBehavior.CLOBBER is an error and can result in data-corruption. " +
81+
"This occured while trying to save " + parameters.getModelClass());
82+
}
83+
6184
try {
6285
return encryptor.encryptRecord(
63-
parameters.getAttributeValues(),
86+
attributeValues,
6487
metadata.getEncryptionFlags(),
6588
paramsToContext(parameters));
6689
} catch (Exception ex) {

src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticTests.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ public class TransformerHolisticTests {
115115
private static final EncryptionMaterialsProvider symWrappedProv;
116116

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

293296
@Test
294297
public void simpleSaveLoad() {
295-
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(symProv));
298+
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(symProv));
296299
Mixed obj = new Mixed();
297300
obj.setHashKey(0);
298301
obj.setRangeKey(15);
@@ -322,7 +325,7 @@ public void simpleSaveLoad() {
322325

323326
@Test
324327
public void leadingAndTrailingZeros() {
325-
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(symProv));
328+
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(symProv));
326329
Mixed obj = new Mixed();
327330
obj.setHashKey(0);
328331
obj.setRangeKey(15);
@@ -364,7 +367,7 @@ public void leadingAndTrailingZeros() {
364367

365368
@Test
366369
public void simpleSaveLoadAsym() {
367-
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(asymProv));
370+
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(asymProv));
368371

369372
BaseClass obj = new BaseClass();
370373
obj.setHashKey(0);
@@ -394,7 +397,7 @@ public void simpleSaveLoadAsym() {
394397

395398
@Test
396399
public void simpleSaveLoadHashOnly() {
397-
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(
400+
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(
398401
symProv));
399402

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

412415
@Test
413416
public void simpleSaveLoadKeysOnly() {
414-
DynamoDBMapper mapper = new DynamoDBMapper(client, DynamoDBMapperConfig.DEFAULT, new AttributeEncryptor(
417+
DynamoDBMapper mapper = new DynamoDBMapper(client, CLOBBER_CONFIG, new AttributeEncryptor(
415418
asymProv));
416419

417420
KeysOnly obj = new KeysOnly();

src/test/java/com/amazonaws/services/dynamodbv2/testing/FakeParameters.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,20 @@
2525
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
2626

2727
public class FakeParameters<T> {
28+
public static <T> AttributeTransformer.Parameters<T> getInstance(Class<T> clazz,
29+
Map<String, AttributeValue> attribs, DynamoDBMapperConfig config, String tableName,
30+
String hashKeyName, String rangeKeyName) {
31+
return getInstance(clazz, attribs, config, tableName, hashKeyName, rangeKeyName, false);
32+
}
33+
2834
public static <T> AttributeTransformer.Parameters<T> getInstance(Class<T> clazz,
2935
Map<String, AttributeValue> attribs, DynamoDBMapperConfig config, String tableName,
30-
String hashKeyName, String rangeKeyName) {
36+
String hashKeyName, String rangeKeyName, boolean isPartialUpdate) {
3137

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

6976
private FakeParameters(Class<T> clazz, Map<String, AttributeValue> attribs,
70-
DynamoDBMapperConfig config, String tableName, String hashKeyName, String rangeKeyName) {
77+
DynamoDBMapperConfig config, String tableName, String hashKeyName, String rangeKeyName,
78+
boolean isPartialUpdate) {
7179
super();
7280
this.clazz = clazz;
7381
this.attrs = Collections.unmodifiableMap(attribs);
7482
this.config = config;
7583
this.tableName = tableName;
7684
this.hashKeyName = hashKeyName;
7785
this.rangeKeyName = rangeKeyName;
86+
this.isPartialUpdate = isPartialUpdate;
7887
}
7988

8089
public Map<String, AttributeValue> getAttributeValues() {
@@ -100,4 +109,8 @@ public String getHashKeyName() {
100109
public String getRangeKeyName() {
101110
return rangeKeyName;
102111
}
112+
113+
public boolean isPartialUpdate() {
114+
return isPartialUpdate;
115+
}
103116
}

0 commit comments

Comments
 (0)