diff --git a/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/ciphertext/java/static-aes-hmac-complex-1.json b/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/ciphertext/java/static-aes-hmac-complex-1.json new file mode 100644 index 0000000000..f3a4a67aac --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/ciphertext/java/static-aes-hmac-complex-1.json @@ -0,0 +1,178 @@ +{ + "TableName": [ + { + "hashKey": { + "N": "5" + }, + "intSet": { + "B": "hSyNrutzz/66hy7Aop/U9tubCBYJkStxMs/p/3kNmgZg1i9S4SpyoDQqIgzrAqESJ9swarmKNk7PAdm+DWTLmQ==" + }, + "doubleSet": { + "B": "kha4s0Orm/nlE1Q6YzvHZk+YhU5QVarJLDnmW2Glak6vmqi03wX5X/pQyNKWrHTGPeDsrx5jvdKwYghgVqncCg==" + }, + "intValue": { + "B": "uPS1BBRz8jbWjN6OWvS0KqkzwzK6mBevD2XKQLMhTRA=" + }, + "stringSet": { + "B": "/OCTLd5eHaLdcgGNKwY5FXiPPEHaOB71Izt7UH6E/IesOWJ4ye39UV00g4K84xbIVBVT3Y5aXM0KW1/hSYEeIg==" + }, + "doubleValue": { + "B": "6MibXf75M2xe/DeX5io6BUNGiP052GJhcEJ91n2i7rU=" + }, + "version": { + "N": "0" + }, + "*amzn-ddb-map-sig*": { + "B": "qON5xK4vsIAEa43E2Il/GgARq3DdKMSOAnLCbl/nEL0=" + }, + "rangeKey": { + "N": "7" + }, + "stringValue": { + "B": "WZRiC0H9mHHZsv81uSB3Qw27nF/Iflf+2QwRcDjRAx5xcWhiUbSTr5mYrp5038yY" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "byteArrayValue": { + "B": "0mFT71NFB/YEZdVA9H0lKhI86Djiy7sezbFFcZ7ua3U=" + }, + "complexValue": { + "B": "wBFWEy3Ao2nhzCxxCLNdY1T5Gbqk5HM8I47r/8sz8zoVtfZaPYvi1wUFjEeIe4isMfCCmvvdBT0GSFCOB/0yCmmcPx0rINxsj5vLcJBQdXc=" + } + }, + { + "hashKey": { + "N": "8" + }, + "intSet": { + "NS": ["0", "1", "10", "15", "200"] + }, + "doubleSet": { + "NS": ["-34.2", "-3", "0", "7.6", "15"] + }, + "intValue": { + "N": "123" + }, + "stringSet": { + "SS": ["?", "Cruel", "Goodbye", "World"] + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "0" + }, + "*amzn-ddb-map-sig*": { + "B": "xBoeUzfWxAsm641nvqJhS7tAaILav/LvusuY39mNYYI=" + }, + "rangeKey": { + "N": "10" + }, + "stringValue": { + "S": "Hello world!" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "complexValue": { + "L": [ + { + "M": { + "tags": { + "SS": ["apple", "banana", "cherry"] + } + } + } + ] + } + }, + { + "rangeKey": { + "N": "9" + }, + "hashKey": { + "N": "7" + }, + "stringValue": { + "S": "Hello world!" + }, + "intSet": { + "NS": ["0", "1", "10", "15", "200"] + }, + "doubleSet": { + "NS": ["-34.2", "-3", "0", "7.6", "15"] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "complexValue": { + "L": [ + { + "M": { + "tags": { + "SS": ["apple", "banana", "cherry"] + } + } + } + ] + }, + "intValue": { + "N": "123" + }, + "stringSet": { + "SS": ["?", "Cruel", "Goodbye", "World"] + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "0" + } + }, + { + "hashKey": { + "N": "6" + }, + "intSet": { + "B": "HuutO2N8WwV9l6RQzHYjIgNgcEiIHRQylrvtUV065HObddI5sQXv5AtXWt/MZDZ048yVApCeZDrOzp6peStbiQ==" + }, + "doubleSet": { + "NS": ["-34.2", "-3", "0", "7.6", "15"] + }, + "intValue": { + "N": "123" + }, + "stringSet": { + "B": "GTfT29jFZLl/yWG+617V+4UHP6zWkMM1YemGOUzrQrmd9TtFSlFPDmdaKmfCO5u0f0o4hic6IRNagl6RUVujRw==" + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "0" + }, + "*amzn-ddb-map-sig*": { + "B": "ChQhebhKmivOac3FvVonKiXShL3iixjAIKi8veZa+Jo=" + }, + "rangeKey": { + "N": "8" + }, + "stringValue": { + "S": "Hello world!" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "byteArrayValue": { + "B": "cLy/3hMrfSkRmGkhXD4aYC7j5cFlfca//k5gwaUilew=" + }, + "complexValue": { + "B": "SwP4pvB4goUzz891QwzMmzFhwnUYvxZzQBGlMUNYmciydYrpmBC8zLOOsVW8r34vIWk76pliPuM53F7MFhnusKAR3rLHBsUCvNzjhOC6JBQ=" + } + } + ] +} diff --git a/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/scenarios.json b/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/scenarios.json index 39cd348968..3e6cce6cf3 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/scenarios.json +++ b/DynamoDbEncryption/runtimes/java/src/test/resources/vectors/encrypted_item/scenarios.json @@ -378,6 +378,17 @@ "plaintext": "file://plaintext.json", "ciphertext": "file://ciphertext/java/wrapped-rsa-hmac-1.json", "network": false + }, + { + "version": "v1_complex", + "provider": "static", + "keys": { + "decrypt": "aesKey", + "verify": "hmacKey" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/java/static-aes-hmac-complex-1.json", + "network": false } ] } diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/LegacyTestVectors.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/LegacyTestVectors.java index 7bab27e430..99f00f45d8 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/LegacyTestVectors.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/LegacyTestVectors.java @@ -11,6 +11,7 @@ import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import software.amazon.awssdk.core.SdkBytes; @@ -279,6 +280,10 @@ public static HashMap ActionsFromClass( attributeActionsOnEncrypt.put("version", CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put("doubleValue", CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put("doubleSet", CryptoAction.SIGN_ONLY); + attributeActionsOnEncrypt.put( + "complexValue", + CryptoAction.ENCRYPT_AND_SIGN + ); } else if (value instanceof SignOnly) { attributeActionsOnEncrypt.put(HASH_KEY, CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put(RANGE_KEY, CryptoAction.SIGN_ONLY); @@ -290,6 +295,7 @@ public static HashMap ActionsFromClass( attributeActionsOnEncrypt.put("version", CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put("doubleValue", CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put("doubleSet", CryptoAction.SIGN_ONLY); + attributeActionsOnEncrypt.put("complexValue", CryptoAction.SIGN_ONLY); } else if (value instanceof Untouched) { attributeActionsOnEncrypt.put(HASH_KEY, CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put(RANGE_KEY, CryptoAction.SIGN_ONLY); @@ -301,6 +307,7 @@ public static HashMap ActionsFromClass( attributeActionsOnEncrypt.put("version", CryptoAction.DO_NOTHING); attributeActionsOnEncrypt.put("doubleValue", CryptoAction.DO_NOTHING); attributeActionsOnEncrypt.put("doubleSet", CryptoAction.DO_NOTHING); + attributeActionsOnEncrypt.put("complexValue", CryptoAction.DO_NOTHING); } else if (value instanceof BaseClass) { // This is a BaseClass attributeActionsOnEncrypt.put(HASH_KEY, CryptoAction.SIGN_ONLY); @@ -322,6 +329,10 @@ public static HashMap ActionsFromClass( CryptoAction.ENCRYPT_AND_SIGN ); attributeActionsOnEncrypt.put("doubleSet", CryptoAction.ENCRYPT_AND_SIGN); + attributeActionsOnEncrypt.put( + "complexValue", + CryptoAction.ENCRYPT_AND_SIGN + ); } else if (value instanceof KeysOnly) { attributeActionsOnEncrypt.put(HASH_KEY, CryptoAction.SIGN_ONLY); attributeActionsOnEncrypt.put(RANGE_KEY, CryptoAction.SIGN_ONLY); @@ -393,6 +404,21 @@ public static BaseClass attributeMapToBaseClass( .collect(Collectors.toSet()) ); } + if (attributeMap.containsKey("complexValue")) { + java.util.List>> complex = + attributeMap + .get("complexValue") + .l() + .stream() + .map(av -> { + java.util.Map> m = + new java.util.HashMap<>(); + av.m().forEach((k, v) -> m.put(k, new java.util.HashSet<>(v.ss()))); + return m; + }) + .collect(java.util.stream.Collectors.toList()); + value.setComplexValue(complex); + } return value; } @@ -435,6 +461,18 @@ public static Map baseClassToV1AttrMap( .collect(java.util.stream.Collectors.toList()) ) ); + if (obj.getComplexValue() != null) { + java.util.List listItems = obj + .getComplexValue() + .stream() + .map(m -> { + Map avMap = new HashMap<>(); + m.forEach((k, v) -> avMap.put(k, new AttributeValue().withSS(v))); + return new AttributeValue().withM(avMap); + }) + .collect(java.util.stream.Collectors.toList()); + map.put("complexValue", new AttributeValue().withL(listItems)); + } return map; } @@ -531,6 +569,40 @@ > baseClassToAttributeMap(Object TEST_VALUE) { ) .build() ); + if (((BaseClass) TEST_VALUE).getComplexValue() != null) { + java.util.List< + software.amazon.awssdk.services.dynamodb.model.AttributeValue + > listItems = + ((BaseClass) TEST_VALUE).getComplexValue() + .stream() + .map(m -> { + Map< + String, + software.amazon.awssdk.services.dynamodb.model.AttributeValue + > avMap = new HashMap<>(); + m.forEach((k, v) -> + avMap.put( + k, + software.amazon.awssdk.services.dynamodb.model.AttributeValue + .builder() + .ss(v) + .build() + ) + ); + return software.amazon.awssdk.services.dynamodb.model.AttributeValue + .builder() + .m(avMap) + .build(); + }) + .collect(java.util.stream.Collectors.toList()); + attributes.put( + "complexValue", + software.amazon.awssdk.services.dynamodb.model.AttributeValue + .builder() + .l(listItems) + .build() + ); + } } else if (TEST_VALUE instanceof KeysOnly) { attributes.put( HASH_KEY, diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java index 891054fcde..8ce858e813 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java @@ -405,6 +405,9 @@ public void decryptTestVector(Scenario scenario) throws IOException { case "v1": assertVersionCompatibility_2(mapper); break; + case "v1_complex": + assertVersionCompatibility_complex(mapper); + break; default: throw new IllegalStateException( "Version " + @@ -507,6 +510,23 @@ public void decryptLegacyTestVector(Scenario scenario) throws IOException { // This is why this vector exists but is not tested. // assertTrue(LegacyTestVectors.testDecryptionTestVector(client, legacyEncryptor, UNTOUCHED_TEST_VALUE_2)); break; + case "v1_complex": + LegacyTestVectors.decryptBaseClassTestVector( + client, + legacyEncryptor, + withComplexValue(ENCRYPTED_TEST_VALUE_2) + ); + LegacyTestVectors.decryptBaseClassTestVector( + client, + legacyEncryptor, + withComplexValue(MIXED_TEST_VALUE_2) + ); + LegacyTestVectors.decryptBaseClassTestVector( + client, + legacyEncryptor, + withComplexValue(SIGNED_TEST_VALUE_2) + ); + break; default: throw new IllegalStateException( "Version " + @@ -515,6 +535,9 @@ public void decryptLegacyTestVector(Scenario scenario) throws IOException { ); } + if ("v1_complex".equals(scenario.version)) { + return; + } LegacyTestVectors.decryptHashKeyOnlyTestVector( client, legacyEncryptor, @@ -1159,6 +1182,77 @@ private void assertVersionCompatibility_2(DynamoDBMapper mapper) { } } + private static final java.util.List< + java.util.Map> + > COMPLEX_VALUE = java.util.Collections.singletonList( + new java.util.HashMap>() { + { + put( + "tags", + new java.util.HashSet<>( + java.util.Arrays.asList("banana", "apple", "cherry") + ) + ); + } + } + ); + + @SuppressWarnings("unchecked") + private static T withComplexValue(T source) { + try { + T copy = (T) source.getClass().getDeclaredConstructor().newInstance(); + copy.setHashKey(source.getHashKey()); + copy.setRangeKey(source.getRangeKey()); + copy.setVersion(source.getVersion()); + copy.setIntValue(source.getIntValue()); + copy.setStringValue(source.getStringValue()); + copy.setByteArrayValue(source.getByteArrayValue()); + copy.setStringSet(source.getStringSet()); + copy.setIntSet(source.getIntSet()); + copy.setDoubleValue(source.getDoubleValue()); + copy.setDoubleSet(source.getDoubleSet()); + copy.setComplexValue(COMPLEX_VALUE); + return copy; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private void assertVersionCompatibility_complex(DynamoDBMapper mapper) { + assertEquals( + withComplexValue(UNTOUCHED_TEST_VALUE_2), + mapper.load( + UNTOUCHED_TEST_VALUE_2.getClass(), + UNTOUCHED_TEST_VALUE_2.getHashKey(), + UNTOUCHED_TEST_VALUE_2.getRangeKey() + ) + ); + assertEquals( + withComplexValue(SIGNED_TEST_VALUE_2), + mapper.load( + SIGNED_TEST_VALUE_2.getClass(), + SIGNED_TEST_VALUE_2.getHashKey(), + SIGNED_TEST_VALUE_2.getRangeKey() + ) + ); + assertEquals( + withComplexValue(MIXED_TEST_VALUE_2), + mapper.load( + MIXED_TEST_VALUE_2.getClass(), + MIXED_TEST_VALUE_2.getHashKey(), + MIXED_TEST_VALUE_2.getRangeKey() + ) + ); + assertEquals( + withComplexValue(ENCRYPTED_TEST_VALUE_2), + mapper.load( + ENCRYPTED_TEST_VALUE_2.getClass(), + ENCRYPTED_TEST_VALUE_2.getHashKey(), + ENCRYPTED_TEST_VALUE_2.getRangeKey() + ) + ); + } + // Prints all current tables in the expected test vector format. // You may need to edit the output to grab the tables you care about, or // separate the tables into separate files for test vectors (metastores e.g.). diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueDeserializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueDeserializer.java index 89021f3002..12954e5e57 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueDeserializer.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueDeserializer.java @@ -57,6 +57,31 @@ public AttributeValue deserialize(JsonParser jp, DeserializationContext ctxt) new TypeReference>() {} ); return new AttributeValue().withNS(numSet); + case "L": + java.util.List listItems = + new java.util.ArrayList<>(); + for (JsonNode element : value) { + JsonParser elemParser = objectMapper.treeAsTokens(element); + elemParser.nextToken(); + listItems.add(deserialize(elemParser, ctxt)); + } + return new AttributeValue().withL(listItems); + case "M": + java.util.Map mapItems = + new java.util.LinkedHashMap<>(); + for ( + java.util.Iterator> mapIter = + value.fields(); + mapIter.hasNext(); + ) { + java.util.Map.Entry entry = mapIter.next(); + JsonParser entryParser = objectMapper.treeAsTokens( + entry.getValue() + ); + entryParser.nextToken(); + mapItems.put(entry.getKey(), deserialize(entryParser, ctxt)); + } + return new AttributeValue().withM(mapItems); default: throw new IllegalStateException( "DDB JSON type " + diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueMatcher.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueMatcher.java index f09c693392..5b8387cd51 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueMatcher.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueMatcher.java @@ -18,6 +18,8 @@ import java.math.BigDecimal; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -60,6 +62,24 @@ public static boolean attrEquals(AttributeValue e, AttributeValue a) { ) { return false; } + List eL = e.getL(); + List aL = a.getL(); + if ((eL == null) != (aL == null)) return false; + if (eL != null) { + if (eL.size() != aL.size()) return false; + for (int i = 0; i < eL.size(); i++) { + if (!attrEquals(eL.get(i), aL.get(i))) return false; + } + } + Map eM = e.getM(); + Map aM = a.getM(); + if ((eM == null) != (aM == null)) return false; + if (eM != null) { + if (!eM.keySet().equals(aM.keySet())) return false; + for (String key : eM.keySet()) { + if (!attrEquals(eM.get(key), aM.get(key))) return false; + } + } return true; } diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueSerializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueSerializer.java index 922380ffaa..256f2beb1a 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueSerializer.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/AttributeValueSerializer.java @@ -41,6 +41,23 @@ public void serialize( jgen.writeString(num); } jgen.writeEndArray(); + } else if (value.getL() != null && !value.getL().isEmpty()) { + jgen.writeFieldName("L"); + jgen.writeStartArray(); + for (AttributeValue item : value.getL()) { + serialize(item, jgen, provider); + } + jgen.writeEndArray(); + } else if (value.getM() != null && !value.getM().isEmpty()) { + jgen.writeFieldName("M"); + jgen.writeStartObject(); + for (java.util.Map.Entry entry : value + .getM() + .entrySet()) { + jgen.writeFieldName(entry.getKey()); + serialize(entry.getValue(), jgen, provider); + } + jgen.writeEndObject(); } else { throw new IllegalStateException( "AttributeValue has no value or type not implemented for serialization." diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/types/BaseClass.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/types/BaseClass.java index e42e3c886d..f1b82513ec 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/types/BaseClass.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv1/com/amazonaws/services/dynamodbv2/testing/types/BaseClass.java @@ -19,6 +19,8 @@ import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBVersionAttribute; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Set; @DynamoDBTable(tableName = "TableName") @@ -38,6 +40,8 @@ public int hashCode() { prime * result + ((stringValue == null) ? 0 : stringValue.hashCode()); result = prime * result + Double.valueOf(doubleValue).hashCode(); result = prime * result + ((doubleSet == null) ? 0 : doubleSet.hashCode()); + result = + prime * result + ((complexValue == null) ? 0 : complexValue.hashCode()); return result; } @@ -63,6 +67,9 @@ public boolean equals(Object obj) { if (doubleSet == null) { if (other.doubleSet != null) return false; } else if (!doubleSet.equals(other.doubleSet)) return false; + if (complexValue == null) { + if (other.complexValue != null) return false; + } else if (!complexValue.equals(other.complexValue)) return false; return true; } @@ -76,6 +83,7 @@ public boolean equals(Object obj) { private Integer version; private double doubleValue; private Set doubleSet; + private List>> complexValue; @DynamoDBHashKey public int getHashKey() { @@ -143,6 +151,14 @@ public void setDoubleSet(Set doubleSet) { this.doubleSet = doubleSet; } + public List>> getComplexValue() { + return complexValue; + } + + public void setComplexValue(List>> complexValue) { + this.complexValue = complexValue; + } + public double getDoubleValue() { return doubleValue; } @@ -181,6 +197,8 @@ public String toString() { doubleValue + ", doubleSet=" + doubleSet + + ", complexValue=" + + complexValue + ", version=" + version + "]" diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/HolisticIT.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/HolisticIT.java index 87598548f5..ce8b888a5e 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/HolisticIT.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/HolisticIT.java @@ -648,6 +648,9 @@ public void decryptTestVector(Scenario scenario) case "v1": assertVersionCompatibility_2(provider, tableName); break; + case "v1_complex": + assertVersionCompatibility_complex(provider, tableName); + break; default: throw new IllegalStateException( "Version " + @@ -706,9 +709,17 @@ public void encryptWithTestVector(Scenario scenario) throws IOException { metastore, scenario.materialDescription ); - Set tables = generateStandardData(provider); + Set tables; + if ("v1_complex".equals(scenario.version)) { + tables = generateComplexData(provider); + } else { + tables = generateStandardData(provider); + } - if (saveToJson && "v1".equals(scenario.version)) { + if ( + saveToJson && + ("v1".equals(scenario.version) || "v1_complex".equals(scenario.version)) + ) { String outputFile = java.nio.file.Paths .get(scenario.ciphertextPath.replace("file://", "")) .getFileName() @@ -1209,6 +1220,97 @@ public Set generateStandardData(EncryptionMaterialsProvider prov) { return tables; } + public Set generateComplexData(EncryptionMaterialsProvider prov) { + Set tables = new LinkedHashSet<>(); + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(prov); + EncryptionContext encryptionContext = EncryptionContext + .builder() + .tableName(tableName) + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + + Map setMap = new HashMap<>(); + setMap.put( + "tags", + AttributeValue.builder().ss("banana", "apple", "cherry").build() + ); + AttributeValue complexAttr = AttributeValue + .builder() + .l(AttributeValue.builder().m(setMap).build()) + .build(); + + Map item; + Map> actions; + + // encrypted + item = new HashMap<>(ENCRYPTED_TEST_VALUE_2); + item.put("complexValue", complexAttr); + actions = new HashMap<>(); + for (String attr : item.keySet()) { + switch (attr) { + case "hashKey": + case "rangeKey": + case "version": + actions.put(attr, signOnly); + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + tables.add(tableName); + putItems( + encryptor.encryptRecord(item, actions, encryptionContext), + tableName + ); + + // mixed + item = new HashMap<>(MIXED_TEST_VALUE_2); + item.put("complexValue", complexAttr); + actions = new HashMap<>(); + for (String attr : item.keySet()) { + switch (attr) { + case "rangeKey": + case "hashKey": + case "version": + case "stringValue": + case "doubleValue": + case "doubleSet": + actions.put(attr, signOnly); + break; + case "intValue": + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + putItems( + encryptor.encryptRecord(item, actions, encryptionContext), + tableName + ); + + // signed + item = new HashMap<>(SIGNED_TEST_VALUE_2); + item.put("complexValue", complexAttr); + actions = new HashMap<>(); + for (String attr : item.keySet()) { + actions.put(attr, signOnly); + } + putItems( + encryptor.encryptRecord(item, actions, encryptionContext), + tableName + ); + + // untouched + item = new HashMap<>(UNTOUCHED_TEST_VALUE_2); + item.put("complexValue", complexAttr); + putItems(item, tableName); + + return tables; + } + protected void putItems(Map map, String tableName) { PutItemRequest request = PutItemRequest .builder() @@ -1529,6 +1631,147 @@ assert new DdbRecordMatcher(UNTOUCHED_TEST_VALUE_2, false) } } + private void assertVersionCompatibility_complex( + EncryptionMaterialsProvider provider, + String tableName + ) throws java.security.GeneralSecurityException { + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(provider); + EncryptionContext encryptionContext = EncryptionContext + .builder() + .tableName(tableName) + .hashKeyName("hashKey") + .rangeKeyName("rangeKey") + .build(); + + Map setMap = new HashMap<>(); + setMap.put( + "tags", + AttributeValue.builder().ss("banana", "apple", "cherry").build() + ); + AttributeValue complexAttr = AttributeValue + .builder() + .l(AttributeValue.builder().m(setMap).build()) + .build(); + + Map expectedEncrypted = new HashMap<>( + ENCRYPTED_TEST_VALUE_2 + ); + expectedEncrypted.put("complexValue", complexAttr); + Map expectedMixed = new HashMap<>( + MIXED_TEST_VALUE_2 + ); + expectedMixed.put("complexValue", complexAttr); + Map expectedSigned = new HashMap<>( + SIGNED_TEST_VALUE_2 + ); + expectedSigned.put("complexValue", complexAttr); + Map expectedUntouched = new HashMap<>( + UNTOUCHED_TEST_VALUE_2 + ); + expectedUntouched.put("complexValue", complexAttr); + + // untouched + HashMap key = new HashMap<>(); + key.put("hashKey", expectedUntouched.get("hashKey")); + key.put("rangeKey", expectedUntouched.get("rangeKey")); + assertTrue( + new com.amazonaws.services.dynamodbv2.datamodeling.sdkv2.testing.DdbRecordMatcher( + expectedUntouched, + false + ) + .matches(getItems(key, tableName)) + ); + + // signed + key = new HashMap<>(); + key.put("hashKey", expectedSigned.get("hashKey")); + key.put("rangeKey", expectedSigned.get("rangeKey")); + Map> actions = new HashMap<>(); + for (String attr : expectedSigned.keySet()) { + actions.put(attr, signOnly); + } + assertTrue( + new com.amazonaws.services.dynamodbv2.datamodeling.sdkv2.testing.DdbRecordMatcher( + expectedSigned, + false + ) + .matches( + encryptor.decryptRecord( + getItems(key, tableName), + actions, + encryptionContext + ) + ) + ); + + // mixed + key = new HashMap<>(); + key.put("hashKey", expectedMixed.get("hashKey")); + key.put("rangeKey", expectedMixed.get("rangeKey")); + actions = new HashMap<>(); + for (String attr : expectedMixed.keySet()) { + switch (attr) { + case "rangeKey": + case "hashKey": + case "version": + case "stringValue": + case "doubleValue": + case "doubleSet": + actions.put(attr, signOnly); + break; + case "intValue": + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + assertTrue( + new com.amazonaws.services.dynamodbv2.datamodeling.sdkv2.testing.DdbRecordMatcher( + expectedMixed, + false + ) + .matches( + encryptor.decryptRecord( + getItems(key, tableName), + actions, + encryptionContext + ) + ) + ); + + // encrypted + key = new HashMap<>(); + key.put("hashKey", expectedEncrypted.get("hashKey")); + key.put("rangeKey", expectedEncrypted.get("rangeKey")); + actions = new HashMap<>(); + for (String attr : expectedEncrypted.keySet()) { + switch (attr) { + case "hashKey": + case "rangeKey": + case "version": + actions.put(attr, signOnly); + break; + default: + actions.put(attr, encryptAndSign); + break; + } + } + assertTrue( + new com.amazonaws.services.dynamodbv2.datamodeling.sdkv2.testing.DdbRecordMatcher( + expectedEncrypted, + false + ) + .matches( + encryptor.decryptRecord( + getItems(key, tableName), + actions, + encryptionContext + ) + ) + ); + } + private static String stripFilePath(String path) { return path.replaceFirst("file://", ""); } diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueDeserializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueDeserializer.java index d197ce324d..8b9612818a 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueDeserializer.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueDeserializer.java @@ -57,6 +57,31 @@ public AttributeValue deserialize(JsonParser jp, DeserializationContext ctxt) new TypeReference>() {} ); return AttributeValue.builder().ns(numSet).build(); + case "L": + java.util.List listItems = + new java.util.ArrayList<>(); + for (JsonNode element : value) { + JsonParser elemParser = objectMapper.treeAsTokens(element); + elemParser.nextToken(); + listItems.add(deserialize(elemParser, ctxt)); + } + return AttributeValue.builder().l(listItems).build(); + case "M": + java.util.Map mapItems = + new java.util.LinkedHashMap<>(); + for ( + java.util.Iterator> mapIter = + value.fields(); + mapIter.hasNext(); + ) { + java.util.Map.Entry entry = mapIter.next(); + JsonParser entryParser = objectMapper.treeAsTokens( + entry.getValue() + ); + entryParser.nextToken(); + mapItems.put(entry.getKey(), deserialize(entryParser, ctxt)); + } + return AttributeValue.builder().m(mapItems).build(); default: throw new IllegalStateException( "DDB JSON type " + diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueMatcher.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueMatcher.java index 45723325ac..c9b5257370 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueMatcher.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueMatcher.java @@ -3,6 +3,8 @@ import java.math.BigDecimal; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -46,6 +48,24 @@ public static boolean attrEquals(AttributeValue e, AttributeValue a) { ) { return false; } + if (e.hasL() != a.hasL()) return false; + if (e.hasL()) { + List eL = e.l(); + List aL = a.l(); + if (eL.size() != aL.size()) return false; + for (int i = 0; i < eL.size(); i++) { + if (!attrEquals(eL.get(i), aL.get(i))) return false; + } + } + if (e.hasM() != a.hasM()) return false; + if (e.hasM()) { + Map eM = e.m(); + Map aM = a.m(); + if (!eM.keySet().equals(aM.keySet())) return false; + for (String key : eM.keySet()) { + if (!attrEquals(eM.get(key), aM.get(key))) return false; + } + } return true; } diff --git a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueSerializer.java b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueSerializer.java index 7c108422a8..fd9a8c3011 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueSerializer.java +++ b/DynamoDbEncryption/runtimes/java/src/test/sdkv2/com/amazonaws/services/dynamodbv2/datamodeling/sdkv2/testing/AttributeValueSerializer.java @@ -40,6 +40,23 @@ public void serialize( jgen.writeString(num); } jgen.writeEndArray(); + } else if (value.hasL()) { + jgen.writeFieldName("L"); + jgen.writeStartArray(); + for (AttributeValue item : value.l()) { + serialize(item, jgen, provider); + } + jgen.writeEndArray(); + } else if (value.hasM()) { + jgen.writeFieldName("M"); + jgen.writeStartObject(); + for (java.util.Map.Entry entry : value + .m() + .entrySet()) { + jgen.writeFieldName(entry.getKey()); + serialize(entry.getValue(), jgen, provider); + } + jgen.writeEndObject(); } else { throw new IllegalStateException( "AttributeValue has no value or type not implemented for serialization."