Skip to content

Commit e637808

Browse files
authored
feat(EnhancedClient): Detect and Error on Ignored DynamoDB Encryption Tags (#248)
1 parent f46f914 commit e637808

File tree

8 files changed

+1044
-100
lines changed

8 files changed

+1044
-100
lines changed

DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEnhancedClientEncryption.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
77
import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata;
88
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
9+
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
10+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
911

10-
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
12+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
1113
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbEncryptionException;
14+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
1215
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;
16+
1317
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
14-
import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
1518

1619
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DoNothingTag.CUSTOM_DDB_ENCRYPTION_DO_NOTHING_PREFIX;
1720
import static software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.SignOnlyTag.CUSTOM_DDB_ENCRYPTION_SIGN_ONLY_PREFIX;
@@ -82,6 +85,9 @@ private static DynamoDbTableEncryptionConfig getTableConfig(DynamoDbEnhancedTabl
8285
// non-key attributes are ENCRYPT_AND_SIGN unless otherwise annotated
8386
actions.put(attributeName, CryptoAction.ENCRYPT_AND_SIGN);
8487
}
88+
89+
// Detect Encryption Flags that are Ignored b/c they are in a Nested Class
90+
scanForIgnoredEncryptionTagsShallow(configWithSchema, attributeName);
8591
}
8692

8793
DynamoDbTableEncryptionConfig.Builder builder = DynamoDbTableEncryptionConfig.builder();
@@ -115,4 +121,48 @@ private static DynamoDbTableEncryptionConfig getTableConfig(DynamoDbEnhancedTabl
115121
.legacyOverride(configWithSchema.legacyOverride())
116122
.build();
117123
}
124+
125+
/**
126+
* Detects DynamoDB Encryption Tags in Nested Enhanced Types.<p>
127+
* This method ONLY parses ONE Layer of nesting.<p>
128+
* It does NOT traverse further nested Enhanced Types.<p>
129+
* DynamoDB Encryption Tags in Nested Classes are IGNORED by the
130+
* Database Encryption SDK for DynamoDB.<p>
131+
* As such, Detection of a nested DynamoDB Encryption Tag on a Nested Type
132+
* triggers a Runtime Exception that MUST NOT BE ignored.<p>
133+
* CAVEAT: Encryption Tags on fields of Nested Classes that are
134+
* Flattened onto the top record are Respected.<p>
135+
* The behavior of Flatten pushes the Attributes onto the top level record,
136+
* making the "flattened sub-fields" equivalent to any other DynamoDB Attribute.<p>
137+
* However, there still exists a possibility for IGNORED Encryption Tags,
138+
* as any Encryption Tag on the field that will be "flattened" is ignored.<p>
139+
* This method DOES NOT detect these "ignored-by-flattened" tags.
140+
*/
141+
private static void scanForIgnoredEncryptionTagsShallow(
142+
final DynamoDbEnhancedTableEncryptionConfig configWithSchema,
143+
final String attributeName
144+
) {
145+
AttributeConverter attributeConverter = configWithSchema.schemaOnEncrypt().converterForAttribute(attributeName);
146+
if (
147+
Objects.nonNull(attributeConverter) &&
148+
Objects.nonNull(attributeConverter.type()) &&
149+
attributeConverter.type().tableSchema().isPresent()
150+
) {
151+
Object maybeTableSchema = attributeConverter.type().tableSchema().get();
152+
if (maybeTableSchema instanceof TableSchema) {
153+
TableSchema subTableSchema = (TableSchema) maybeTableSchema;
154+
if (
155+
subTableSchema.tableMetadata().customMetadata().containsKey(CUSTOM_DDB_ENCRYPTION_SIGN_ONLY_PREFIX) ||
156+
subTableSchema.tableMetadata().customMetadata().containsKey(CUSTOM_DDB_ENCRYPTION_DO_NOTHING_PREFIX)
157+
) {
158+
throw DynamoDbEncryptionException.builder()
159+
.message(String.format(
160+
"Detected a DynamoDbEncryption Tag/Configuration on a nested attribute of %s. " +
161+
"This is NOT Supported at this time!",
162+
attributeName))
163+
.build();
164+
}
165+
}
166+
}
167+
}
118168
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
7+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
8+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
9+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
10+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
11+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
12+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
13+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
14+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
15+
16+
/**
17+
* This is a valid use of DynamoDbEncryption annotations attributes with
18+
* DynamoDbConvertedBy.<p>
19+
* The DynamoDbEncryption annotations are placed on elements that are converted
20+
* to Maps.<p>
21+
* In this case, only {@code nestedEncrypted} will be written to the DynamoDB Table as a
22+
* binary. {@code nestedSigned} and {@code nestedIgnored} are recorded as DynamoDB Maps.
23+
*/
24+
@DynamoDbBean
25+
public class AnnotatedConvertedBy {
26+
private String partitionKey;
27+
private int sortKey;
28+
private ConvertedByNestedBean nestedEncrypted;
29+
private ConvertedByNestedBean nestedSigned;
30+
private ConvertedByNestedBean nestedIgnored;
31+
32+
@DynamoDbPartitionKey
33+
@DynamoDbAttribute(value = "partition_key")
34+
public String getPartitionKey() {
35+
return this.partitionKey;
36+
}
37+
public void setPartitionKey(String partitionKey) {
38+
this.partitionKey = partitionKey;
39+
}
40+
41+
@DynamoDbSortKey
42+
@DynamoDbAttribute(value = "sort_key")
43+
public int getSortKey() {
44+
return this.sortKey;
45+
}
46+
public void setSortKey(int sortKey) {
47+
this.sortKey = sortKey;
48+
}
49+
50+
@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
51+
@DynamoDbAttribute("nestedEncrypted")
52+
public ConvertedByNestedBean getNestedEncrypted() {
53+
return this.nestedEncrypted;
54+
}
55+
public void setNestedEncrypted(ConvertedByNestedBean nested) {
56+
this.nestedEncrypted = nested;
57+
}
58+
59+
@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
60+
@DynamoDbEncryptionSignOnly
61+
@DynamoDbAttribute("nestedSigned")
62+
public ConvertedByNestedBean getNestedSigned() {
63+
return this.nestedSigned;
64+
}
65+
public void setNestedSigned(ConvertedByNestedBean nested) {
66+
this.nestedSigned = nested;
67+
}
68+
69+
@DynamoDbConvertedBy(ConvertedByNestedBean.NestedBeanConverter.class)
70+
@DynamoDbEncryptionDoNothing
71+
@DynamoDbAttribute("nestedIgnored")
72+
public ConvertedByNestedBean getNestedIgnored() {
73+
return this.nestedIgnored;
74+
}
75+
public void setNestedIgnored(ConvertedByNestedBean nested) {
76+
this.nestedIgnored = nested;
77+
}
78+
79+
public static class ConvertedByNestedBean {
80+
private String id;
81+
private String firstName;
82+
private String lastName;
83+
84+
// A Default Empty constructor is needed by the Enhanced Client
85+
public ConvertedByNestedBean() {};
86+
87+
public ConvertedByNestedBean(String id, String firstName, String lastName) {
88+
this.id = id;
89+
this.firstName = firstName;
90+
this.lastName = lastName;
91+
}
92+
93+
public String getId() { return this.id; }
94+
public void setId(String id) { this.id = id; }
95+
96+
public String getFirstName() { return firstName; }
97+
public void setFirstName(String firstName) { this.firstName = firstName; }
98+
99+
public String getLastName() { return lastName; }
100+
public void setLastName(String lastName) { this.lastName = lastName; }
101+
102+
@Override
103+
public boolean equals(Object obj) {
104+
if (this == obj) return true;
105+
if (obj == null) return false;
106+
if (getClass() != obj.getClass()) return false;
107+
ConvertedByNestedBean other = (ConvertedByNestedBean) obj;
108+
if (id == null) {
109+
if (other.getId() != null) return false;
110+
} else if (!id.equals(other.getId())) return false;
111+
if (firstName == null) {
112+
if (other.getFirstName() != null) return false;
113+
} else if (!firstName.equals(other.getFirstName())) return false;
114+
if (lastName == null) {
115+
if (other.getLastName() != null) return false;
116+
} else if (!lastName.equals(other.getLastName())) return false;
117+
return true;
118+
}
119+
120+
public static final class NestedBeanConverter implements AttributeConverter<ConvertedByNestedBean> {
121+
@Override
122+
public AttributeValue transformFrom(ConvertedByNestedBean ConvertedByNestedBean) {
123+
Map<String, AttributeValue> map = new HashMap<>(3);
124+
map.put("id", AttributeValue.fromS(ConvertedByNestedBean.getId()));
125+
map.put("firstName", AttributeValue.fromS(ConvertedByNestedBean.getFirstName()));
126+
map.put("lastName", AttributeValue.fromS(ConvertedByNestedBean.getLastName()));
127+
return AttributeValue.fromM(map);
128+
}
129+
130+
@Override
131+
public ConvertedByNestedBean transformTo(AttributeValue attributeValue) {
132+
Map<String, AttributeValue> map = attributeValue.m();
133+
return new ConvertedByNestedBean(
134+
map.get("id").s(),
135+
map.get("firstName").s(),
136+
map.get("lastName").s());
137+
}
138+
139+
@Override
140+
public EnhancedType<ConvertedByNestedBean> type() {
141+
return EnhancedType.of(ConvertedByNestedBean.class);
142+
}
143+
144+
@Override
145+
public AttributeValueType attributeValueType() {
146+
return AttributeValueType.M;
147+
}
148+
}
149+
}
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient;
2+
3+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
4+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
5+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
6+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
7+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
8+
9+
/**
10+
* This is a valid use of DynamoDbEncryption annotations on nested attributes.
11+
* Currently, the ONLY WAY to properly specify DynamoDbEncryption actions
12+
* on nested attributes is via the @DynamoDbFlatten attribute.<p>
13+
*
14+
* Note that you MUST NOT use a DynamoDbEncryption annotation on the
15+
* Flattened Attribute itself.<p>
16+
* Only on it's sub-fields.<p>
17+
*/
18+
@DynamoDbBean
19+
public class AnnotatedFlattenedBean {
20+
private String partitionKey;
21+
private int sortKey;
22+
private FlattenedNestedBean nestedBeanClass;
23+
24+
@DynamoDbPartitionKey
25+
@DynamoDbAttribute(value = "partition_key")
26+
public String getPartitionKey() {
27+
return this.partitionKey;
28+
}
29+
30+
public void setPartitionKey(String partitionKey) {
31+
this.partitionKey = partitionKey;
32+
}
33+
34+
@DynamoDbSortKey
35+
@DynamoDbAttribute(value = "sort_key")
36+
public int getSortKey() {
37+
return this.sortKey;
38+
}
39+
40+
public void setSortKey(int sortKey) {
41+
this.sortKey = sortKey;
42+
}
43+
44+
// Any @DynamoDbEncryption annotation here would be IGNORED
45+
// Instead, the Annotations MUST BE placed on the Getter Methods of
46+
// FlattenedNestedBean.
47+
@DynamoDbFlatten
48+
public FlattenedNestedBean getNestedBeanClass() {
49+
return this.nestedBeanClass;
50+
}
51+
52+
public void setNestedBeanClass(FlattenedNestedBean nestedBeanClass) {
53+
this.nestedBeanClass = nestedBeanClass;
54+
}
55+
56+
@DynamoDbBean
57+
public static class FlattenedNestedBean {
58+
private String id;
59+
private String firstName;
60+
private String lastName;
61+
62+
public FlattenedNestedBean() {};
63+
64+
public FlattenedNestedBean(String id, String firstName, String lastName) {
65+
this.id = id;
66+
this.firstName = firstName;
67+
this.lastName = lastName;
68+
}
69+
70+
@DynamoDbAttribute("id")
71+
public String getId() { return this.id; }
72+
public void setId(String id) { this.id = id; }
73+
74+
@DynamoDbEncryptionSignOnly
75+
@DynamoDbAttribute("firstName")
76+
public String getFirstName() { return firstName; }
77+
public void setFirstName(String firstName) { this.firstName = firstName; }
78+
79+
@DynamoDbEncryptionDoNothing
80+
@DynamoDbAttribute("lastName")
81+
public String getLastName() { return lastName; }
82+
public void setLastName(String lastName) { this.lastName = lastName; }
83+
84+
@Override
85+
public boolean equals(Object obj) {
86+
if (this == obj) return true;
87+
if (obj == null) return false;
88+
if (getClass() != obj.getClass()) return false;
89+
FlattenedNestedBean other = (FlattenedNestedBean) obj;
90+
if (id == null) {
91+
if (other.getId() != null) return false;
92+
} else if (!id.equals(other.getId())) return false;
93+
if (firstName == null) {
94+
if (other.getFirstName() != null) return false;
95+
} else if (!firstName.equals(other.getFirstName())) return false;
96+
if (lastName == null) {
97+
if (other.getLastName() != null) return false;
98+
} else if (!lastName.equals(other.getLastName())) return false;
99+
return true;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)