diff --git a/.github/workflows/ci_permute_java.yml b/.github/workflows/ci_permute_java.yml new file mode 100644 index 000000000..5b4e41d2c --- /dev/null +++ b/.github/workflows/ci_permute_java.yml @@ -0,0 +1,85 @@ +# This workflow performs tests in Java. +name: Library Java tests + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + # Manual trigger for this workflow, either the normal version + # or the nightly build that uses the latest Dafny prerelease + # (accordingly to the "nightly" parameter). + inputs: + nightly: + description: 'Run the nightly build' + required: false + type: boolean + schedule: + # Nightly build against Dafny's nightly prereleases, + # for early warning of verification issues or regressions. + # Timing chosen to be adequately after Dafny's own nightly build, + # but this might need to be tweaked: + # https://github.com/dafny-lang/dafny/blob/master/.github/workflows/deep-tests.yml#L16 + - cron: "30 16 * * *" + +jobs: + testJava: + # Don't run the nightly build on forks + if: github.event_name != 'schedule' || github.repository_owner == 'aws' + strategy: + matrix: + library: [ + DecryptWithPermute + ] + java-version: [ 8, 11, 16, 17 ] + os: [ + # TODO just test on mac for now + #windows-latest, + #ubuntu-latest, + macos-latest + ] + runs-on: ${{ matrix.os }} + permissions: + id-token: write + contents: read + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::370957321024:role/GitHub-CI-DDBEC-Dafny-Role-us-west-2 + role-session-name: DDBEC-Dafny-Java-Tests + + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Dafny + uses: dafny-lang/setup-dafny-action@v1.6.1 + with: + # A && B || C is the closest thing to an if .. then ... else ... or ?: expression the GitHub Actions syntax supports. + dafny-version: ${{ (github.event_name == 'schedule' || inputs.nightly) && 'nightly-latest' || '4.1.0' }} + + - name: Setup Java ${{ matrix.java-version }} + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: ${{ matrix.java-version }} + + - name: Build ${{ matrix.library }} implementation + shell: bash + working-directory: ./${{ matrix.library }} + run: | + # This works because `node` is installed by default on GHA runners + CORES=$(node -e 'console.log(os.cpus().length)') + make build_java CORES=$CORES + + - name: Test ${{ matrix.library }} + working-directory: ./${{ matrix.library }} + run: | + # Clear MPL from cache + # We have to do this because MakeFile does not do this yet. The MakeFile automatically builds and deploys dependencies + # instead it should be picking it up from Maven. + rm -rf ~/.m2/repository/software/amazon/cryptography/aws-cryptographic-material-providers + make test_java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a246fbbd..ecda8d9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 3.1.2 2023-11-13 + +### Fix + +Fixed an issue where, when using the DynamoDbEncryptionInterceptor, +an encrypted item in the Attributes field of a DeleteItem, PutItem, or UpdateItem +response was passed through unmodified instead of being decrypted. + +## 3.1.1 2023-11-07 + +### Fix + +Issue when a DynamoDB Set attribute is marked as SIGN_ONLY in the AWS Database Encryption SDK (DB-ESDK) for DynamoDB. + +DB-ESDK for DynamoDB supports SIGN_ONLY and ENCRYPT_AND_SIGN attribute actions. In version 3.1.0 and below, when a Set type is assigned a SIGN_ONLY attribute action, there is a chance that signature validation of the record containing a Set will fail on read, even if the Set attributes contain the same values. The probability of a failure depends on the order of the elements in the Set combined with how DynamoDB returns this data, which is undefined. + +This update addresses the issue by ensuring that any Set values are canonicalized in the same order while written to DynamoDB as when read back from DynamoDB. + +See: https://github.com/aws/aws-database-encryption-sdk-dynamodb-java/DecryptWithPermute/README.md for additional details + + ## 3.1.0 2023-09-07 ### Features diff --git a/DecryptWithPermute/.gitignore b/DecryptWithPermute/.gitignore new file mode 100644 index 000000000..49e6039c1 --- /dev/null +++ b/DecryptWithPermute/.gitignore @@ -0,0 +1,5 @@ +TestResults +ImplementationFromDafny.cs +TestsFromDafny.cs +**/bin +**/obj diff --git a/DecryptWithPermute/Makefile b/DecryptWithPermute/Makefile new file mode 100644 index 000000000..b7d9726a0 --- /dev/null +++ b/DecryptWithPermute/Makefile @@ -0,0 +1,53 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +CORES=2 + +include ../SharedMakefile.mk + +DIR_STRUCTURE_V2=V2 + +PROJECT_SERVICES := DecryptWithPermute + +# Namespace for each local service +# Currently our build relies on local services and namespaces being 1:1 +SERVICE_NAMESPACE_DecryptWithPermute=aws.cryptography.dbEncryptionSdk.decryptWithPermute + +MAX_RESOURCE_COUNT=20000000 +# Order is important +# In java they MUST be built +# in the order they depend on each other +PROJECT_DEPENDENCIES := \ + submodules/MaterialProviders/AwsCryptographyPrimitives \ + submodules/MaterialProviders/ComAmazonawsKms \ + submodules/MaterialProviders/ComAmazonawsDynamodb \ + submodules/MaterialProviders/AwsCryptographicMaterialProviders \ + DynamoDbEncryption + +STD_LIBRARY=submodules/MaterialProviders/StandardLibrary +SMITHY_DEPS=submodules/MaterialProviders/model + +# Since we are packaging projects differently, we cannot make assumptions +# about where the files are located. +# This is here to get unblocked but should be removed once we have migrated packages +# to the new packaging format. +PROJECT_INDEX := \ + submodules/MaterialProviders/AwsCryptographyPrimitives/src/Index.dfy \ + submodules/MaterialProviders/ComAmazonawsKms/src/Index.dfy \ + submodules/MaterialProviders/ComAmazonawsDynamodb/src/Index.dfy \ + submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographicMaterialProviders/src/Index.dfy \ + submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographyKeyStore/src/Index.dfy + +# Dependencies for each local service +SERVICE_DEPS_DecryptWithPermute := \ + submodules/MaterialProviders/AwsCryptographyPrimitives \ + submodules/MaterialProviders/ComAmazonawsKms \ + submodules/MaterialProviders/ComAmazonawsDynamodb \ + submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographicMaterialProviders \ + submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographyKeyStore \ + DynamoDbEncryption/dafny/DynamoDbEncryption \ + DynamoDbEncryption/dafny/StructuredEncryption \ + DynamoDbEncryption/dafny/DynamoDbItemEncryptor + +format_net: + pushd runtimes/net && dotnet format DynamoDbEncryption.csproj && popd diff --git a/DecryptWithPermute/README.md b/DecryptWithPermute/README.md new file mode 100644 index 000000000..6ca9e5c2d --- /dev/null +++ b/DecryptWithPermute/README.md @@ -0,0 +1,170 @@ +## DecryptWithPermute + +DB-ESDK for DynamoDB supports SIGN_ONLY and ENCRYPT_AND_SIGN attribute actions. +In version 3.1.0 and below, when a Set type is assigned a SIGN_ONLY attribute action, +there is a chance that signature validation of the record containing a Set will fail on read, +even if the Set attributes contain the same values. +The probability of a failure depends on the order of the elements in the Set +combined with how DynamoDB returns this data, which is undefined. + +The 3.1.1 update addresses the issue by ensuring that any Set values are canonicalized +in the same order while written to DynamoDB as when read back from DynamoDB. + +This project implements a slightly modified version of DecryptItem +from the the AWS Database Encryption SDK for DynamoDB, +which can validate an encrypted record that as written with 3.1.0, +allowing it to be decrypted. + +PermuteDecrypt is exactly like the DynamoDbItemEncryptor's DecryptItem, +with one exception : +If the signature fails to match, and SIGN_ONLY Sets are involved, +then other permutations of the members of those sets are tried, +and the item is decrypted if any of those permutations allow the signature to match. + +If you would normally decrypt an item like this +``` +DynamoDbItemEncryptor itemEncryptor = DynamoDbItemEncryptor.builder() + .DynamoDbItemEncryptorConfig(config) + .build(); +DecryptItemOutput decryptedItem = itemEncryptor.DecryptItem(myInput); +``` +You instead do this : +``` +DynamoDbPermuteDecryptor itemDecryptor = DynamoDbPermuteDecryptor.builder() + .DynamoDbPermuteDecryptorConfig.builder() + .inner(config) + .build() + .build(); +PermuteDecryptOutput decryptedItem = itemDecryptor.PermuteDecrypt( + PermuteDecryptInput.builder() + .inner(myInput), + .maxSetSize(7) + .build() + .build(); +``` +The PermuteDecryptInput holds a normal DecryptItemInput object, plus a `maxSetSize`. +If any set in the item has more elements than `maxSetSize`, +decryption of the item is attempted, but no permutations are attempted. + +The output of PermuteDecrypt holds two entries +`inner` : a DecryptItemOutput object +`didPermute` : if false, the item was validated and decrypted with no special handling. +If true, some permutation of sets in the input allowed successful validation. + +If you think you know the set permutations with which the item was originally written, +set that attribute of your item to the expected permutation, and call `PermuteDecrypt` +with a `maxSetSize` of `1`. A single attempt will be made to validate and decrypt the record, +which will quickly succeed or fail. + +To exhaustively try every permutation of a set of size N required N! (N factorial) attempts. +As sets get large (over 7 or 8) this can start to take a considerable amount of time, +so use `maxSetSize` to put a limit on the size of a set that will be attempted. +A set as large as 9 can probably be handled, depending on your hardware, +but over that is probably untenable. + + +### Code Organization + +DecryptWithPermute is a project containing the following Dafny 'localServices' under `dafny`: +- DecryptWithPermute: A single entry point : PermuteDecrypt described above. + +Currently this project only supports Java. + +#### Java + +`runtimes/java` contains the Java related code and build instructions for this project. + +Within `runtimes/java`: + +- `src/main/java` contains all hand written Java code, including externs. +- `src/main/dafny-generated` contains all Dafny to Java transpiled code. +- `src/main/smithy-generated` contains all Smithy to Java generated code. + +### Development + +Common Makefile targets are: + +- `make verify` verifies the whole project. You should specify a `CORES` that is as high as your + computer supports in order to speed this up. However, this will still probably take long enough + that your dev loop should instead use the following: + - You can instead specify a single service to verify via: `make verify_service SERVICE=DecryptWithPermute` + - You can also verify a specific file and output in a more help format via: `make verify_single FILE=`, + where `` is the path to the file to verify relative to this directory (`DynamoDbEncryption`). + You may optionally narrow down the scope by specifying a `PROC`. For example, if you just want to verify + the method `EncryptStructure`, use `make verify_single FILE= PROC=EncryptStructure` +- `make build_java` transpiles, builds, and tests everything from scratch in Java. + You will need to run this the first time you clone this repo, and any time you introduce changes + that end up adding or removing dafny-generated files. + - The above command takes a while to complete. + If you want to re-generate dafny code specific to a service for a service, use the following: + `make local_transpile_impl_java_single SERVICE=DecryptWithPermute FILE=Index.dfy` + and then `test_java` to build/test those changes. + Using `Index.dfy` will end up transpiling the entire service, but you can also specify a different + file to scope down the transpilation further. This target will transpile `FILE` and every + "includes" in that `FILE`, recursively down to the bounds of the service namespace. + Note that the `transpile_implementation_java_single` target is provided as a convenience, + and is not guaranteed to be 100% consistent with output from the regular `build_java` target. + The behavior SHOULD NOT be different, although if you are experiencing + weird behavior, see if `build_java` resolves the issue. + Only use this target for local testing. + - `make local_transpile_test_java_single SERVICE=DecryptWithPermute FILE=Validate.dfy` + may be used similar to above in order to re-transpile a specific test file + (and any of that module's "includes" within `/test`). + Note that this will clobber all other Dafny generated tests being run + with `make test_java`. This target is useful to quickly iterate on changes + to tests within a specific file, but you will need to `make build_java` + again if you want to run the full test suite locally. + Only use this target for local testing. +- `make test_java` builds and tests the transpiled code in Java. + +### Development Requirements + +* Dafny 4.1.0: https://github.com/dafny-lang/dafny +* A Java 8 or newer development environment + +#### (Optional) Dafny Report Generator Requirements + +Optionally, if you want to run the [Dafny Report Generator](#generate-dafny-report) +you will need to install the `dafny-reportgenerator` dotnet tool +(and make sure `.dotnet/tools` is within your `PATH`, +see instructions in the output from running the following command): + +``` +dotnet tool install --global dafny-reportgenerator --version 1.2.0 +``` + +#### (Optional) Duvet Requirements + +Optionally, if you want to run [Duvet](https://github.com/awslabs/duvet) reports, +you will need to use Cargo to install duvet. + +If you don't have it already, +[get and install Cargo and Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html). + +Then install duvet: + +``` +cargo +stable install duvet +``` + +#### System Dependencies - macOS only + +If you are using macOS then you must install OpenSSL 1.1, +and the OpenSSL 1.1 `lib` directory must be on the dynamic linker path at runtime. + +If the .NET runtime cannot locate your OpenSSL 1.1 libraries, +you may encounter an error that says: + +> No usable version of libssl was found + +To resolve this, +we recommend that you install OpenSSL via Homebrew using `brew install openssl@1.1`. + +## Security + +See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. + +## License + +This project is licensed under the Apache-2.0 License. + diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy new file mode 100644 index 000000000..5a3957197 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy @@ -0,0 +1,226 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +include "../../../../submodules/MaterialProviders/StandardLibrary/src/Index.dfy" + include "../../../../DynamoDbEncryption/dafny/DynamoDbEncryption/src/Index.dfy" + include "../../../../DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy" + include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/src/Index.dfy" + include "../../../../submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographicMaterialProviders/src/Index.dfy" + module {:extern "software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types" } AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + { + import opened Wrappers + import opened StandardLibrary.UInt + import opened UTF8 + import AwsCryptographyDbEncryptionSdkDynamoDbTypes + import AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import AwsCryptographyMaterialProvidersTypes + // Generic helpers for verification of mock/unit tests. + datatype DafnyCallEvent = DafnyCallEvent(input: I, output: O) + + // Begin Generated Types + + class IDynamoDbPermuteDecryptorClientCallHistory { + ghost constructor() { + PermuteDecrypt := []; +} + ghost var PermuteDecrypt: seq>> +} + trait {:termination false} IDynamoDbPermuteDecryptorClient + { + // Helper to define any additional modifies/reads clauses. + // If your operations need to mutate state, + // add it in your constructor function: + // Modifies := {your, fields, here, History}; + // If you do not need to mutate anything: +// Modifies := {History}; + + ghost const Modifies: set + // For an unassigned field defined in a trait, + // Dafny can only assign a value in the constructor. + // This means that for Dafny to reason about this value, + // it needs some way to know (an invariant), + // about the state of the object. + // This builds on the Valid/Repr paradigm + // To make this kind requires safe to add + // to methods called from unverified code, + // the predicate MUST NOT take any arguments. + // This means that the correctness of this requires + // MUST only be evaluated by the class itself. + // If you require any additional mutation, + // then you MUST ensure everything you need in ValidState. + // You MUST also ensure ValidState in your constructor. + predicate ValidState() + ensures ValidState() ==> History in Modifies + ghost const History: IDynamoDbPermuteDecryptorClientCallHistory + predicate PermuteDecryptEnsuresPublicly(input: PermuteDecryptInput , output: Result) + // The public method to be called by library consumers + method PermuteDecrypt ( input: PermuteDecryptInput ) + returns (output: Result) + requires + && ValidState() + modifies Modifies - {History} , + History`PermuteDecrypt + // Dafny will skip type parameters when generating a default decreases clause. + decreases Modifies - {History} + ensures + && ValidState() + ensures PermuteDecryptEnsuresPublicly(input, output) + ensures History.PermuteDecrypt == old(History.PermuteDecrypt) + [DafnyCallEvent(input, output)] + +} + datatype DynamoDbPermuteDecryptorConfig = | DynamoDbPermuteDecryptorConfig ( + nameonly inner: AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.DynamoDbItemEncryptorConfig + ) + datatype PermuteDecryptInput = | PermuteDecryptInput ( + nameonly inner: AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.DecryptItemInput , + nameonly maxSetSize: int32 + ) + datatype PermuteDecryptOutput = | PermuteDecryptOutput ( + nameonly inner: AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.DecryptItemOutput , + nameonly didPermute: bool + ) + datatype Error = + // Local Error structures are listed here + | DynamoDbPermuteDecryptorException ( + nameonly message: string + ) + // Any dependent models are listed here + | AwsCryptographyDbEncryptionSdkDynamoDb(AwsCryptographyDbEncryptionSdkDynamoDb: AwsCryptographyDbEncryptionSdkDynamoDbTypes.Error) + | AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor: AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.Error) + | AwsCryptographyDbEncryptionSdkStructuredEncryption(AwsCryptographyDbEncryptionSdkStructuredEncryption: AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.Error) + | AwsCryptographyMaterialProviders(AwsCryptographyMaterialProviders: AwsCryptographyMaterialProvidersTypes.Error) + // The Collection error is used to collect several errors together + // This is useful when composing OR logic. + // Consider the following method: + // + // method FN(n:I) + // returns (res: Result) + // ensures A(I).Success? ==> res.Success? + // ensures B(I).Success? ==> res.Success? + // ensures A(I).Failure? && B(I).Failure? ==> res.Failure? + // + // If either A || B is successful then FN is successful. + // And if A && B fail then FN will fail. + // But what information should FN transmit back to the caller? + // While it may be correct to hide these details from the caller, + // this can not be the globally correct option. + // Suppose that A and B can be blocked by different ACLs, + // and that their representation of I is only eventually consistent. + // How can the caller distinguish, at a minimum for logging, + // the difference between the four failure modes? + // || (!access(A(I)) && !access(B(I))) + // || (!exit(A(I)) && !exit(B(I))) + // || (!access(A(I)) && !exit(B(I))) + // || (!exit(A(I)) && !access(B(I))) + | CollectionOfErrors(list: seq, nameonly message: string) + // The Opaque error, used for native, extern, wrapped or unknown errors + | Opaque(obj: object) + type OpaqueError = e: Error | e.Opaque? witness * +} + abstract module AbstractAwsCryptographyDbEncryptionSdkDecryptWithPermuteService + { + import opened Wrappers + import opened StandardLibrary.UInt + import opened UTF8 + import opened Types = AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + import Operations : AbstractAwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations + function method DefaultDynamoDbPermuteDecryptorConfig(): DynamoDbPermuteDecryptorConfig + method DynamoDbPermuteDecryptor(config: DynamoDbPermuteDecryptorConfig := DefaultDynamoDbPermuteDecryptorConfig()) + returns (res: Result) + requires config.inner.keyring.Some? ==> + config.inner.keyring.value.ValidState() + requires config.inner.cmm.Some? ==> + config.inner.cmm.value.ValidState() + requires config.inner.legacyOverride.Some? ==> + config.inner.legacyOverride.value.encryptor.ValidState() + modifies if config.inner.keyring.Some? then + config.inner.keyring.value.Modifies + else {} + modifies if config.inner.cmm.Some? then + config.inner.cmm.value.Modifies + else {} + modifies if config.inner.legacyOverride.Some? then + config.inner.legacyOverride.value.encryptor.Modifies + else {} + ensures res.Success? ==> + && fresh(res.value) + && fresh(res.value.Modifies + - ( if config.inner.keyring.Some? then + config.inner.keyring.value.Modifies + else {} + ) - ( if config.inner.cmm.Some? then + config.inner.cmm.value.Modifies + else {} + ) - ( if config.inner.legacyOverride.Some? then + config.inner.legacyOverride.value.encryptor.Modifies + else {} + ) ) + && fresh(res.value.History) + && res.value.ValidState() + ensures config.inner.keyring.Some? ==> + config.inner.keyring.value.ValidState() + ensures config.inner.cmm.Some? ==> + config.inner.cmm.value.ValidState() + ensures config.inner.legacyOverride.Some? ==> + config.inner.legacyOverride.value.encryptor.ValidState() + + class DynamoDbPermuteDecryptorClient extends IDynamoDbPermuteDecryptorClient + { + constructor(config: Operations.InternalConfig) + requires Operations.ValidInternalConfig?(config) + ensures + && ValidState() + && fresh(History) + && this.config == config + const config: Operations.InternalConfig + predicate ValidState() + ensures ValidState() ==> + && Operations.ValidInternalConfig?(config) + && History !in Operations.ModifiesInternalConfig(config) + && Modifies == Operations.ModifiesInternalConfig(config) + {History} + predicate PermuteDecryptEnsuresPublicly(input: PermuteDecryptInput , output: Result) + {Operations.PermuteDecryptEnsuresPublicly(input, output)} + // The public method to be called by library consumers + method PermuteDecrypt ( input: PermuteDecryptInput ) + returns (output: Result) + requires + && ValidState() + modifies Modifies - {History} , + History`PermuteDecrypt + // Dafny will skip type parameters when generating a default decreases clause. + decreases Modifies - {History} + ensures + && ValidState() + ensures PermuteDecryptEnsuresPublicly(input, output) + ensures History.PermuteDecrypt == old(History.PermuteDecrypt) + [DafnyCallEvent(input, output)] + { + output := Operations.PermuteDecrypt(config, input); + History.PermuteDecrypt := History.PermuteDecrypt + [DafnyCallEvent(input, output)]; +} + +} +} + abstract module AbstractAwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations { + import opened Wrappers + import opened StandardLibrary.UInt + import opened UTF8 + import opened Types = AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + type InternalConfig + predicate ValidInternalConfig?(config: InternalConfig) + function ModifiesInternalConfig(config: InternalConfig): set + predicate PermuteDecryptEnsuresPublicly(input: PermuteDecryptInput , output: Result) + // The private method to be refined by the library developer + + + method PermuteDecrypt ( config: InternalConfig , input: PermuteDecryptInput ) + returns (output: Result) + requires + && ValidInternalConfig?(config) + modifies ModifiesInternalConfig(config) + // Dafny will skip type parameters when generating a default decreases clause. + decreases ModifiesInternalConfig(config) + ensures + && ValidInternalConfig?(config) + ensures PermuteDecryptEnsuresPublicly(input, output) +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/Model/DynamoDbPermuteDecryptor.smithy b/DecryptWithPermute/dafny/DecryptWithPermute/Model/DynamoDbPermuteDecryptor.smithy new file mode 100644 index 000000000..706a08b9a --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/Model/DynamoDbPermuteDecryptor.smithy @@ -0,0 +1,81 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +namespace aws.cryptography.dbEncryptionSdk.decryptWithPermute + +use aws.polymorph#localService +use aws.polymorph#javadoc +use aws.polymorph#reference + +use aws.cryptography.dbEncryptionSdk.dynamoDb.itemEncryptor#DynamoDbItemEncryptorConfig +use aws.cryptography.dbEncryptionSdk.dynamoDb.itemEncryptor#DecryptItemInput +use aws.cryptography.dbEncryptionSdk.dynamoDb.itemEncryptor#DecryptItemOutput + +use aws.cryptography.dbEncryptionSdk.structuredEncryption#StructuredEncryption +use aws.cryptography.dbEncryptionSdk.dynamoDb.itemEncryptor#DynamoDbItemEncryptor +use aws.cryptography.dbEncryptionSdk.dynamoDb#DynamoDbEncryption +use aws.cryptography.materialProviders#AwsCryptographicMaterialProviders + + +@localService( + sdkId: "DynamoDbPermuteDecryptor", + config: DynamoDbPermuteDecryptorConfig, + dependencies: [ + AwsCryptographicMaterialProviders, + StructuredEncryption, + DynamoDbItemEncryptor, + DynamoDbEncryption + ] +) +service DynamoDbPermuteDecryptor { + version: "2022-08-26", + operations: [PermuteDecrypt], + errors: [DynamoDbPermuteDecryptorException], +} + +@javadoc("The configuration for the client-side encryption of DynamoDB items.") +structure DynamoDbPermuteDecryptorConfig { + @required + inner: DynamoDbItemEncryptorConfig +} + + +@javadoc("Decrypt a DynamoDB Item, permute sets as necessary to match signature.") +operation PermuteDecrypt { + input: PermuteDecryptInput, + output: PermuteDecryptOutput +} + +structure PermuteDecryptInput { + @required + inner: DecryptItemInput, + @required + @range(min: 0) + maxSetSize: Integer +} + +@javadoc("Outputs for decrypting a DynamoDB Item.") +structure PermuteDecryptOutput { + @required + inner: DecryptItemOutput, + @required + didPermute: Boolean +} + +@aws.polymorph#reference(service: aws.cryptography.dbEncryptionSdk.dynamoDb#DynamoDbEncryption) +structure DynamoDbEncryptionReference {} + +@aws.polymorph#reference(service: aws.cryptography.materialProviders#AwsCryptographicMaterialProviders) +structure AwsCryptographicMaterialProvidersReference {} + +@aws.polymorph#reference(service: aws.cryptography.dbEncryptionSdk.structuredEncryption#StructuredEncryption) +structure StructuredEncryptionReference {} + +///////////// +// Errors + +@error("client") +structure DynamoDbPermuteDecryptorException { + @required + message: String, +} + diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDecryptStructure.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDecryptStructure.dfy new file mode 100644 index 000000000..5ee72f14e --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDecryptStructure.dfy @@ -0,0 +1,360 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +include "../Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy" +include "../../../../submodules/MaterialProviders/libraries/src/Collections/Maps/Maps.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/Model/AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/src/Footer.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/src/Paths.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/src/Util.dfy" +include "ValidatePermuted.dfy" + +module AltDecryptStructure { + import opened Wrappers + import opened StandardLibrary + import opened StandardLibrary.UInt + import opened AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + + import SEU = StructuredEncryptionUtil + import Base64 + import CMP = AwsCryptographyMaterialProvidersTypes + import CSE = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import Prim = AwsCryptographyPrimitivesTypes + import Random + import Aws.Cryptography.Primitives + import Header = StructuredEncryptionHeader + import Footer = StructuredEncryptionFooter + import MaterialProviders + import Materials + import Crypt = StructuredEncryptionCrypt + import Paths = StructuredEncryptionPaths + import SortedSets + import Seq + import Digest + import Defaults + import HKDF + import AlgorithmSuites + import Maps + import SEO = AwsCryptographyDbEncryptionSdkStructuredEncryptionOperations + import ValidatePermuted + + function method E(s : string) : Error { + DynamoDbPermuteDecryptorException(message := s) + } + + // given a list of fields, return only those that should be encrypted, according to the legend + function method {:tailrecursion} {:opaque} FilterEncrypted(fields : seq, legend : Header.Legend) + : (ret : seq) + requires |fields| == |legend| + ensures forall k <- ret :: k in fields + { + if |fields| == 0 then + [] + else if legend[0] == Header.ENCRYPT_AND_SIGN_LEGEND then + [fields[0]] + FilterEncrypted(fields[1..], legend[1..]) + else + FilterEncrypted(fields[1..], legend[1..]) + } + + // Fail unless the field exists, and is a binary terminal + function method {:opaque} NeedBinary(data : CSE.StructuredDataMap, field : string): (result: Outcome) + { + if field !in data then + Fail(E("The field name " + field + " is required.")) + else if !data[field].content.Terminal? then + Fail(E(field + " must be a Terminal.")) + else if data[field].content.Terminal.typeId != SEU.BYTES_TYPE_ID then + Fail(E(field + " must be a binary Terminal.")) + else + Pass + } + + // Return the sum of the sizes of the given fields + function method {:opaque} SumValueSize(fields : seq, data : SEU.StructuredDataCanon) + : nat + requires forall k <- fields :: k in data + { + if |fields| == 0 then + 0 + else + |data[fields[0]].content.Terminal.value| + SumValueSize(fields[1..], data) + } + + function method {:opaque} GetAlgorithmSuiteId(alg : Option) + : (ret : CMP.AlgorithmSuiteId) + ensures + && (alg.Some? ==> ret == CMP.AlgorithmSuiteId.DBE(alg.value)) + && (alg.None? ==> ret == CMP.AlgorithmSuiteId.DBE(CMP.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384)) + { + if alg.Some? then + CMP.AlgorithmSuiteId.DBE(alg.value) + else + CMP.AlgorithmSuiteId.DBE(CMP.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384) + } + + type DecryptCanon = c: DecryptCanonData | ValidDecryptCanon?(c) + witness * + + // for Decrypt, the data necessary to construct the Intermediate Encrypted Structured Data + datatype DecryptCanonData = DecryptCanonData ( + encFields_c : seq, // These fields were encrypted, sorted. + // i.e. a Crypto Action of ENCRYPT_AND_SIGN + signedFields_c : seq,// These fields were signed, sorted + // i.e. an Authenticate Action of SIGN + data_c : SEU.StructuredDataCanon, // All signed fields with canonized paths + // i.e. the Intermediate Encrypted Structured Data, properly encrypted + cryptoSchema : CSE.CryptoSchema // The crypto schema calculated from the crypto legend. + // This value is returned as part of the Parsed Header. + ) + + predicate ValidDecryptCanon?(c: DecryptCanonData) { + && (forall k :: k in c.data_c.Keys ==> k in c.signedFields_c) + && (forall k :: k in c.signedFields_c ==> k in c.data_c.Keys) + && (forall k :: k in c.encFields_c ==> k in c.signedFields_c) + && |c.encFields_c| < (UINT32_LIMIT / 3) + && c.cryptoSchema.content.SchemaMap? + && var actionMap := c.cryptoSchema.content.SchemaMap; + && |c.data_c| == |actionMap| + && (exists tableName :: (forall k :: k in actionMap ==> Paths.SimpleCanon(tableName, k) in c.data_c)) + } + + // return the subset of "fields" which are ENCRYPT_AND_SIGN + function method {:tailrecursion} {:opaque} FilterEncrypt(fields : seq, fieldMap : SEU.CanonMap, schema : SEU.CryptoSchemaPlain) + : (ret : seq) + requires forall k <- fields :: k in fieldMap + requires forall k <- fieldMap :: fieldMap[k] in schema + ensures forall k <- ret :: k in fields + { + if |fields| == 0 then + [] + else + var act := schema[fieldMap[fields[0]]].content.Action; + if act == CSE.ENCRYPT_AND_SIGN then + [fields[0]] + FilterEncrypt(fields[1..], fieldMap, schema) + else + FilterEncrypt(fields[1..], fieldMap, schema) + } + + function method GetFieldMap(tableName : SEU.GoodString, data : SEU.StructuredDataPlain, schema : SEU.CryptoSchemaPlain) + : (ret : map) + requires schema.Keys == data.Keys + ensures forall k <- data :: schema[k].content.Action == CSE.DO_NOTHING || Paths.SimpleCanon(tableName, k) in ret + ensures Maps.Injective(ret) + { + reveal Maps.Injective(); + Paths.SimpleCanonUnique(tableName); + map k <- data | schema[k].content.Action != CSE.DO_NOTHING :: Paths.SimpleCanon(tableName, k) := k + } + + + // construct the DecryptCanon + function method {:opaque} {:vcs_split_on_every_assert} CanonizeForDecrypt( + tableName: SEU.GoodString, + data: SEU.StructuredDataPlain, + authSchema: SEU.AuthSchemaPlain, + legend: Header.Legend + ) : (ret : Result) + requires authSchema.Keys == data.Keys + ensures ret.Success? ==> + && |ret.value.signedFields_c| == |legend| + ensures ret.Success? ==> + && (forall k :: k in data.Keys && authSchema[k].content.Action.SIGN? ==> Paths.SimpleCanon(tableName, k) in ret.value.data_c.Keys) + ensures ret.Success? ==> + && (forall v :: v in ret.value.data_c.Values ==> v in data.Values) + ensures ret.Success? ==> + && ret.value.cryptoSchema.content.SchemaMap? + && SEU.CryptoSchemaMapIsFlat(ret.value.cryptoSchema.content.SchemaMap) + && SEU.AuthSchemaIsFlat(authSchema) + && ValidParsedCryptoSchema(ret.value.cryptoSchema.content.SchemaMap, authSchema, tableName) + { + reveal Maps.Injective(); + Paths.SimpleCanonUnique(tableName); + var fieldMap := map k <- data | authSchema[k].content.Action == CSE.SIGN :: + Paths.SimpleCanon(tableName, k) := k; + assert Maps.Injective(fieldMap); + + var data_c := map k <- fieldMap :: k := data[fieldMap[k]]; + var signedFields_c := SortedSets.ComputeSetToOrderedSequence2(data_c.Keys, SEU.ByteLess); + + if |legend| < |signedFields_c| then + Failure(E("Schema changed : something that was unsigned is now signed.")) + else + if |legend| > |signedFields_c| then + Failure(E("Schema changed : something that was signed is now unsigned.")) + else + + var encFields_c : seq := FilterEncrypted(signedFields_c, legend); + :- Need(|encFields_c| < (UINT32_LIMIT / 3), E("Too many encrypted fields.")); + + var actionMap := map k <- fieldMap :: + fieldMap[k] := if Paths.SimpleCanon(tableName, fieldMap[k]) in encFields_c then + CSE.CryptoSchema( + content := CSE.CryptoSchemaContent.Action(CSE.ENCRYPT_AND_SIGN), + attributes := None + ) + else + CSE.CryptoSchema( + content := CSE.CryptoSchemaContent.Action(CSE.SIGN_ONLY), + attributes := None + ); + var cryptoSchema := CSE.CryptoSchema( + content := CSE.CryptoSchemaContent.SchemaMap(actionMap), + attributes := None + ); + + var c := DecryptCanonData( + encFields_c, + signedFields_c, + data_c, + cryptoSchema + ); + + assert |data_c| == |actionMap| by { + assert data_c.Keys == fieldMap.Keys; + assert actionMap.Keys == fieldMap.Values; + LemmaInjectiveImpliesUniqueValues(fieldMap); + } + + assert exists tableName :: + (forall k :: k in c.cryptoSchema.content.SchemaMap ==> Paths.SimpleCanon(tableName, k) in c.data_c); + + Success(c) + } + + predicate ValidParsedCryptoSchema(cryptoSchema: CSE.CryptoSchemaMap, authSchema: CSE.AuthenticateSchemaMap, tableName: SEU.GoodString) + requires SEU.AuthSchemaIsFlat(authSchema) + requires SEU.CryptoSchemaMapIsFlat(cryptoSchema) + { + // Every field in the crypto map exists in the auth map as SIGN + && (forall k <- cryptoSchema.Keys :: k in authSchema && authSchema[k].content.Action.SIGN?) + // The crypto map is not missing any SIGN fields from the auth map + && (forall kv <- authSchema.Items | kv.1.content.Action.SIGN? :: kv.0 in cryptoSchema.Keys) + // Every field in the crypto map is ENCRYPT_AND_SIGN or SIGN_ONLY + && (forall v <- cryptoSchema.Values :: v.content.Action.SIGN_ONLY? || v.content.Action.ENCRYPT_AND_SIGN?) + } + + const ReservedAuthMap : SEU.AuthSchemaPlain := map[ + SEU.HeaderField := SEU.DoNotSign, // The header field is authenticated in the footer via a separate mechanism + SEU.FooterField := SEU.DoNotSign + ] + + datatype AltDecryptStructureOutput = AltDecryptStructureOutput( + inner : CSE.DecryptStructureOutput, + didPermute : bool + ) + + method {:vcs_split_on_every_assert} AltDecryptStructure (config: SEO.InternalConfig, input: CSE.DecryptStructureInput, maxSetSize : nat) + returns (output: Result) + modifies SEO.ModifiesInternalConfig(config) + requires SEO.ValidInternalConfig?(config) + ensures SEO.ValidInternalConfig?(config) + requires input.cmm.ValidState() + modifies input.cmm.Modifies + ensures input.cmm.ValidState() + { + :- Need(input.authenticateSchema.content.SchemaMap?, E("Authenticate Schema must be a SchemaMap")); + :- Need(SEU.AuthSchemaIsFlat(input.authenticateSchema.content.SchemaMap), E("Schema must be flat.")); + :- Need(forall k <- input.authenticateSchema.content.SchemaMap :: SEU.ValidString(k), E("Schema has bad field name.")); + :- Need(forall k <- input.authenticateSchema.content.SchemaMap | k in ReservedAuthMap :: + input.authenticateSchema.content.SchemaMap[k] == ReservedAuthMap[k], E("Reserved fields in Schema must be DO_NOT_SIGN.")); + var authSchema : SEU.AuthSchemaPlain := input.authenticateSchema.content.SchemaMap + ReservedAuthMap; + + :- Need(input.encryptedStructure.content.DataMap?, E("Input structure must be a DataMap")); + :- Need(SEU.DataMapIsFlat(input.encryptedStructure.content.DataMap), E("Input DataMap must be flat.")); + :- Need(authSchema.Keys == input.encryptedStructure.content.DataMap.Keys, E("Authenticate schema must match encrypted structure exactly.")); + var encRecord : SEU.StructuredDataPlain := input.encryptedStructure.content.DataMap; + :- NeedBinary(encRecord, SEU.HeaderField); + :- NeedBinary(encRecord, SEU.FooterField); + :- Need(exists x :: (x in input.authenticateSchema.content.SchemaMap && input.authenticateSchema.content.SchemaMap[x].content.Action == CSE.SIGN), E("At least one Authenticate Action must be SIGN")); + + var headerSerialized := encRecord[SEU.HeaderField].content.Terminal.value; + var footerSerialized := encRecord[SEU.FooterField].content.Terminal.value; + var head :- Header.PartialDeserialize(headerSerialized).MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + var headerAlgorithmSuiteR := head.GetAlgorithmSuite(config.materialProviders); + var headerAlgorithmSuite :- headerAlgorithmSuiteR.MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + + var matR := input.cmm.DecryptMaterials( + CMP.DecryptMaterialsInput ( + algorithmSuiteId := headerAlgorithmSuite.id, + commitmentPolicy := SEO.DBE_COMMITMENT_POLICY, + encryptedDataKeys := head.dataKeys, + encryptionContext := head.encContext, + reproducedEncryptionContext := input.encryptionContext + ) + ); + var matOutput :- matR.MapFailure(e => AwsCryptographyMaterialProviders(e)); + var mat := matOutput.decryptionMaterials; + :- Need(Header.ValidEncryptionContext(mat.encryptionContext), E("Bad encryption context")); + :- Need(Materials.DecryptionMaterialsWithPlaintextDataKey(mat), E("Encryption material has no key")); + + :- Need(SEU.ValidSuite(mat.algorithmSuite), E("Invalid Algorithm Suite")); + var postCMMAlg := mat.algorithmSuite; + var key := mat.plaintextDataKey.value; + var commitKeyR := Crypt.GetCommitKey(config.primitives, postCMMAlg, key, head.msgID); + var commitKey :- commitKeyR.MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + var okR := head.verifyCommitment(config.primitives, postCMMAlg, commitKey, headerSerialized); + var ok :- okR.MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + + :- Need(SEU.ValidString(input.tableName), E("Bad Table Name")); + var canonData :- CanonizeForDecrypt(input.tableName, encRecord, authSchema, head.legend); + + assert |head.legend| == |canonData.signedFields_c|; + + var footer :- Footer.DeserializeFooter(footerSerialized, postCMMAlg.signature.ECDSA?).MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + + var val := footer.validate(config.primitives, mat, head.dataKeys, + canonData.signedFields_c, canonData.encFields_c, map[], canonData.data_c, headerSerialized); + if val.Success? { + var innerR := SEO.DecryptStructure(config, input); + var inner :- innerR.MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + return Success(AltDecryptStructureOutput(inner := inner, didPermute := false)); + } + + var val2 :- ValidatePermuted.Validate(footer, config.primitives, mat, head.dataKeys, + canonData.signedFields_c, canonData.encFields_c, map[], canonData.data_c, headerSerialized, maxSetSize); + + var repairedData : SEU.StructuredDataPlain := map k <- encRecord | true :: k := + var c := Paths.SimpleCanon(input.tableName, k); + if c in val2 then + val2[c] + else + encRecord[k]; + + var innerR := SEO.DecryptStructure(config, input.(encryptedStructure := CSE.StructuredData(content := CSE.DataMap(repairedData), attributes := None))); + var inner :- innerR.MapFailure(e => AwsCryptographyDbEncryptionSdkStructuredEncryption(e)); + return Success(AltDecryptStructureOutput(inner := inner, didPermute := true)); + } + + // predicates/lemmas like this are not yet provided out of the box in the standard library. + predicate {:opaque} Contains(big: map, small: map) + { + && small.Keys <= big.Keys + && forall x <- small :: small[x] == big[x] + } + + lemma LemmaContainsPreservesInjectivity(big: map, small: map) + requires Contains(big, small) + requires Maps.Injective(big) + ensures Maps.Injective(small) + { + reveal Contains(); + reveal Maps.Injective(); + } + + lemma LemmaInjectiveImpliesUniqueValues(m: map) + requires Maps.Injective(m) + ensures |m.Keys| == |m.Values| + { + if |m| > 0 { + var x: X :| x in m; + var y := m[x]; + var m' := Maps.Remove(m, x); + reveal Contains(); + assert Contains(m, m'); + + reveal Maps.Injective(); + assert m'.Values == m.Values - {y}; + LemmaContainsPreservesInjectivity(m, m'); + LemmaInjectiveImpliesUniqueValues(m'); + } + } +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDynamoToStruct.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDynamoToStruct.dfy new file mode 100644 index 000000000..326a39145 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/AltDynamoToStruct.dfy @@ -0,0 +1,1141 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +include "../Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy" +include "../../../../DynamoDbEncryption/dafny/DynamoDbEncryption/src/NormalizeNumber.dfy" + +module AltDynamoToStruct { + + import opened ComAmazonawsDynamodbTypes + import opened AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import opened Wrappers + import opened StandardLibrary + import opened StandardLibrary.UInt + import opened DynamoDbEncryptionUtil + import AwsCryptographyDbEncryptionSdkDynamoDbTypes + import UTF8 + import SortedSets + import Seq + import Norm = DynamoDbNormalizeNumber + + type Error = AwsCryptographyDbEncryptionSdkDynamoDbTypes.Error + + type StructuredDataTerminalType = x : StructuredData | x.content.Terminal? witness * + type TerminalDataMap = map + + // This file exists for these two functions : ItemToStructured and StructuredToItem + // which provide conversion between an AttributeMap and a StructuredDataMap + + // Convert AttributeMap to StructuredDataMap + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - MUST be a [Structured Data Map](../structured-encryption/structures.md#structured-data-map). + function method {:opaque} ItemToStructured(item : AttributeMap) : (ret : Result) + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - MUST contain a [Structured Data Terminal](../structured-encryption/structures.md#structured-data-terminal) + //# for each attribute on the DynamoDB Item, and no others. + ensures ret.Success? ==> ret.value.Keys == item.Keys + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - MUST NOT have [Structured Data Attributes](../structured-encryption/structures.md#structured-data-attributes). + ensures ret.Success? ==> forall v <- ret.value.Values :: v.content.Terminal? + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - The [Terminal Type ID](../structured-encryption/structures.md#terminal-type-id) for each attribute MUST + //# be the [Type ID](./ddb-attribute-serialization.md#type-id) of the [serialization](./ddb-attribute-serialization.md) of this Attribute Value. + ensures ret.Success? ==> forall kv <- ret.value.Items :: kv.1.content.Terminal.typeId == AttrToTypeId(item[kv.0]) + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - The Structured Data Terminal MUST be located at the top level of the Structured Data, + //# string indexed by the Attribute Name. + ensures ret.Success? ==> forall kv <- ret.value.Items :: kv.0 in ret.value.Keys && ret.value[kv.0].content.Terminal? + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data + //= type=implication + //# - The [Terminal Value](../structured-encryption/structures.md#terminal-value) for each attribute MUST + //# be the [Value](./ddb-attribute-serialization.md#type-id) of the [serialization](./ddb-attribute-serialization.md) of this Attribute Value. + ensures ret.Success? ==> forall kv <- ret.value.Items :: + && TopLevelAttributeToBytes(item[kv.0]).Success? + && kv.1.content.Terminal.value == TopLevelAttributeToBytes(item[kv.0]).value + + { + var structuredMap := map k <- item :: k := AttrToStructured(item[k]); + MapKeysMatchItems(item); + MapError(SimplifyMapValue(structuredMap)) + } + + // Convert StructuredDataMap to AttributeMap + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - MUST be a [Structured Data Map](../structured-encryption/structures.md#structured-data-map). + function method {:opaque} StructuredToItem(s : StructuredDataMap) : (ret : Result) + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - MUST contain an Attribute for every [Structured Data Terminal](../structured-encryption/structures.md#structured-data-terminal) + //# on the Structured Data, and not other Attributes. + ensures ret.Success? ==> ret.value.Keys == s.Keys + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - MUST NOT have any `Key` strings that are invalid DynamoDB AttributeNames, that is, with more than 65535 characters. + ensures ret.Success? ==> forall k <- s.Keys :: IsValid_AttributeName(k) + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - Each Attribute MUST be deserializable + //# according to [the serialization scheme](./ddb-attribute-serialization.md#value). + ensures ret.Success? ==> forall kv <- ret.value.Items :: + && StructuredToAttr(s[kv.0]).Success? + && kv.1 == StructuredToAttr(s[kv.0]).value + { + if forall k <- s.Keys :: IsValid_AttributeName(k) then + var structuredData := map k <- s :: k := StructuredToAttr(s[k]); + MapKeysMatchItems(s); + MapError(SimplifyMapValue(structuredData)) + else + var badNames := set k <- s | !IsValid_AttributeName(k) :: k; + OneBadKey(s, badNames, IsValid_AttributeName); + var orderedAttrNames := SetToOrderedSequence(badNames, CharLess); + var attrNameList := Join(orderedAttrNames, ","); + MakeError("Not valid attribute names : " + attrNameList) + } + + // everything past here is to implement those two + + // Prove round trip. A work in progress + lemma RoundTripFromItem(item : AttributeValue) + ensures item.B? && AttrToStructured(item).Success? ==> StructuredToAttr(AttrToStructured(item).value).Success? + ensures item.NULL? && AttrToStructured(item).Success? ==> + && StructuredToAttr(AttrToStructured(item).value).Success? + ensures item.BOOL? && AttrToStructured(item).Success? ==> StructuredToAttr(AttrToStructured(item).value).Success? + { + reveal AttrToStructured(); + reveal StructuredToAttr(); + reveal TopLevelAttributeToBytes(); + reveal AttrToBytes(); + reveal BytesToAttr(); + + } + + // Prove round trip. A work in progress + lemma RoundTripFromStructured(s : StructuredData) + ensures StructuredToAttr(s).Success? && s.content.Terminal.typeId == BINARY ==> + && AttrToStructured(StructuredToAttr(s).value).Success? + ensures StructuredToAttr(s).Success? && s.content.Terminal.typeId == BOOLEAN ==> + && AttrToStructured(StructuredToAttr(s).value).Success? + ensures StructuredToAttr(s).Success? && s.content.Terminal.typeId == NULL ==> + && AttrToStructured(StructuredToAttr(s).value).Success? +{ + reveal AttrToStructured(); + reveal StructuredToAttr(); + reveal TopLevelAttributeToBytes(); + reveal AttrToBytes(); + reveal BytesToAttr(); + } + + function method MakeError(s : string) : Result { + Failure(Error.DynamoDbEncryptionException(message := s)) + } + + function method MapError(r : Result) : Result { + if r.Success? then + Success(r.value) + else + MakeError(r.error) + } + + function method {:opaque} TopLevelAttributeToBytes(a : AttributeValue) : Result, string> + // We never need to prefix at the top level. + // The Type ID is serialized separately, + // and the Length is not needed since we are + // working with the exact set of bytes when we + // need to deserialize. + { + AttrToBytes(a, false) + } + + function method {:opaque} AttrToStructured(item : AttributeValue) : (ret : Result) + ensures ret.Success? ==> ret.value.content.Terminal? + ensures ret.Success? ==> ret.value.content.Terminal.typeId == AttrToTypeId(item) + ensures ret.Success? ==> + && TopLevelAttributeToBytes(item).Success? + && ret.value.content.Terminal.value == TopLevelAttributeToBytes(item).value + { + var body :- TopLevelAttributeToBytes(item); + Success(StructuredData(content := Terminal(StructuredDataTerminal(value := body, typeId := AttrToTypeId(item))), attributes := None)) + } + + function method {:opaque} StructuredToAttr(s : StructuredData) : (ret : Result) + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - This [Structured Data Map](../structured-encryption/structures.md#structured-data-map), + //# if not empty, + //# MUST only contain [Structured Data Terminals](../structured-encryption/structures.md#structured-data-terminal). + ensures ret.Success? ==> s.content.Terminal? + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-structured-data-to-ddb-item + //= type=implication + //# - MUST NOT have [Structured Data Attributes](../structured-encryption/structures.md#structured-data-attributes). + ensures ret.Success? ==> s.attributes.None? + { + :- Need(s.attributes.None?, "attributes must be None"); + :- Need(s.content.Terminal?, "StructuredData to AttributeValue only works on Terminal data"); + + var Terminal(s) := s.content; + :- Need(|s.typeId| == 2, "Type ID must be two bytes"); + var attrValueAndLength :- BytesToAttr(s.value, s.typeId, false); + :- Need(attrValueAndLength.len == |s.value|, "Mismatch between length of encoded data and length of data"); + Success(attrValueAndLength.val) + } + + const BOOL_LEN : nat := 1 // number of bytes in an encoded boolean + const TYPEID_LEN : nat := 2 // number of bytes in a TerminalTypeId + const LENGTH_LEN : nat := 4 // number of bytes in an encoded count or length + const PREFIX_LEN : nat := 6 // number of bytes in a prefix, i.e. 2-byte type and 4-byte length + + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#type-id + //= type=implication + //# Type ID indicates what type a DynamoDB Attribute Value MUST + //# be serialized and deserialized as. + //# | Attribute Value Data Type | Terminal Type ID | + //# | ------------------------- | ---------------- | + //# | Null (NULL) | 0x0000 | + //# | String (S) | 0x0001 | + //# | Number (N) | 0x0002 | + //# | Binary (B) | 0xFFFF | + //# | Boolean (BOOL) | 0x0004 | + //# | String Set (SS) | 0x0101 | + //# | Number Set (NS) | 0x0102 | + //# | Binary Set (BS) | 0x01FF | + //# | Map (M) | 0x0200 | + //# | List (L) | 0x0300 | + const TERM_T : uint8 := 0x00 + const SET_T : uint8 := 0x01 + const MAP_T : uint8 := 0x02 + const LIST_T : uint8 := 0x03 + const NULL_T : uint8 := 0x00 + const STRING_T : uint8 := 0x01 + const NUMBER_T : uint8 := 0x02 + const BINARY_T : uint8 := 0xFF + const BOOLEAN_T : uint8 := 0x04 + + const NULL : TerminalTypeId := [TERM_T, NULL_T] + const STRING : TerminalTypeId := [TERM_T, STRING_T] + const NUMBER : TerminalTypeId := [TERM_T, NUMBER_T] + const BINARY : TerminalTypeId := [0xFF, 0xFF] + const BOOLEAN : TerminalTypeId := [TERM_T, BOOLEAN_T] + const STRING_SET : TerminalTypeId := [SET_T, STRING_T] + const NUMBER_SET : TerminalTypeId := [SET_T, NUMBER_T] + const BINARY_SET : TerminalTypeId := [SET_T, BINARY_T] + const MAP : TerminalTypeId := [MAP_T, NULL_T] + const LIST : TerminalTypeId := [LIST_T, NULL_T] + + function method AttrToTypeId(a : AttributeValue) : TerminalTypeId + { + match a { + case S(s) => STRING + case N(n) => NUMBER + case B(b) => BINARY + case SS(ss) => STRING_SET + case NS(ns) => NUMBER_SET + case BS(bs) => BINARY_SET + case M(m) => MAP + case L(l) => LIST + case NULL(n) => NULL + case BOOL(b) => BOOLEAN + } + } + + predicate method CharLess(x : char, y : char) { + x < y + } + + // convert AttributeValue to byte sequence + // if `prefix` is true, prefix sequence with TypeID and Length + function method {:opaque} AttrToBytes(a : AttributeValue, prefix : bool, depth : nat := 1) : (ret : Result, string>) + decreases a + ensures ret.Success? && prefix ==> 6 <= |ret.value| + ensures MAX_STRUCTURE_DEPTH < depth ==> ret.Failure? + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#boolean + //= type=implication + //# Boolean MUST be serialized as: + //# - `0x00` if the value is `false` + //# - `0x01` if the value is `true` + ensures a.BOOL? && !prefix && depth <= MAX_STRUCTURE_DEPTH ==> + && (a.BOOL ==> ret.Success? && |ret.value| == BOOL_LEN && ret.value[0] == 1) + && (!a.BOOL ==> ret.Success? && |ret.value| == BOOL_LEN && ret.value[0] == 0) + ensures a.BOOL? && prefix && depth <= MAX_STRUCTURE_DEPTH ==> + && (a.BOOL ==> (ret.Success? && |ret.value| == PREFIX_LEN+BOOL_LEN && ret.value[PREFIX_LEN] == 1 + && ret.value[0..TYPEID_LEN] == BOOLEAN && ret.value[TYPEID_LEN..PREFIX_LEN] == [0,0,0,1])) + && (!a.BOOL ==> (ret.Success? && |ret.value| == PREFIX_LEN+BOOL_LEN && ret.value[PREFIX_LEN] == 0 + && ret.value[0..TYPEID_LEN] == BOOLEAN && ret.value[TYPEID_LEN..PREFIX_LEN] == [0,0,0,1])) + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#binary + //= type=implication + //# Binary MUST be serialized with the identity function; + //# or more plainly, Binary Attribute Values are used as is. + ensures a.B? && !prefix && depth <= MAX_STRUCTURE_DEPTH ==> ret.Success? && ret.value == a.B + ensures a.B? && prefix && ret.Success? && depth <= MAX_STRUCTURE_DEPTH ==> + && ret.value[PREFIX_LEN..] == a.B + && ret.value[0..TYPEID_LEN] == BINARY + && U32ToBigEndian(|a.B|).Success? + && ret.value[TYPEID_LEN..PREFIX_LEN] == U32ToBigEndian(|a.B|).value + && BigEndianToU32(ret.value[TYPEID_LEN..PREFIX_LEN]).value == |a.B| + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#null + //= type=implication + //# Null MUST be serialized as a zero-length byte string. + ensures a.NULL? && !prefix && depth <= MAX_STRUCTURE_DEPTH ==> ret.Success? && |ret.value| == 0 + ensures a.NULL? && prefix && depth <= MAX_STRUCTURE_DEPTH ==> ret.Success? && |ret.value| == PREFIX_LEN && ret.value[0..TYPEID_LEN] == NULL && ret.value[TYPEID_LEN..PREFIX_LEN] == [0,0,0,0] + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#string + //= type=implication + //# String MUST be serialized as UTF-8 encoded bytes. + ensures a.S? && ret.Success? && !prefix ==> + UTF8.Decode(ret.value).Success? && UTF8.Decode(ret.value).value == a.S + ensures a.S? && ret.Success? && prefix ==> + && UTF8.Decode(ret.value[PREFIX_LEN..]).Success? + && UTF8.Decode(ret.value[PREFIX_LEN..]).value == a.S + && ret.value[0..TYPEID_LEN] == STRING + && UTF8.Encode(a.S).Success? + && U32ToBigEndian(|UTF8.Encode(a.S).value|).Success? + && ret.value[TYPEID_LEN..PREFIX_LEN] == U32ToBigEndian(|UTF8.Encode(a.S).value|).value + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#number + //= type=implication + //# Number MUST be serialized as UTF-8 encoded bytes. + ensures a.N? && ret.Success? && !prefix ==> + && Norm.NormalizeNumber(a.N).Success? + && var nn := Norm.NormalizeNumber(a.N).value; + && UTF8.Decode(ret.value).Success? && UTF8.Decode(ret.value).value == nn + ensures a.N? && ret.Success? && prefix ==> + && Norm.NormalizeNumber(a.N).Success? + && var nn := Norm.NormalizeNumber(a.N).value; + && UTF8.Decode(ret.value[PREFIX_LEN..]).Success? + && UTF8.Decode(ret.value[PREFIX_LEN..]).value == nn + && ret.value[0..TYPEID_LEN] == NUMBER + && UTF8.Encode(nn).Success? + && U32ToBigEndian(|UTF8.Encode(nn).value|).Success? + && ret.value[TYPEID_LEN..PREFIX_LEN] == U32ToBigEndian(|UTF8.Encode(nn).value|).value + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=implication + //# Binary Sets MUST NOT contain duplicate entries. + ensures a.BS? && ret.Success? ==> Seq.HasNoDuplicates(a.BS) + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set + //= type=implication + //# A Set MUST be serialized as: + //# | Field | Length | + //# | ------------ | -------- | + //# | Set Count | 4 | + //# | Set Entries | Variable | + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-count + //= type=implication + //# Set Count MUST be a big-endian unsigned integer + //# equal to the number of serialized entries in + //# [Set Entries](#set-entries). + ensures a.BS? && ret.Success? && !prefix ==> + && U32ToBigEndian(|a.BS|).Success? + && |ret.value| >= LENGTH_LEN + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|a.BS|).value + && BigEndianToU32(ret.value[0..LENGTH_LEN]).value == |a.BS| + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-count + //= type=implication + //# Set Count MAY be `0`, + //# in which case [Set Entries](#set-entries) is a zero-length byte string. + && (|a.BS| == 0 ==> |ret.value| == LENGTH_LEN) + + ensures a.BS? && ret.Success? && prefix ==> + && U32ToBigEndian(|a.BS|).Success? + && |ret.value| >= PREFIX_LEN + LENGTH_LEN + && ret.value[0..TYPEID_LEN] == BINARY_SET + && ret.value[PREFIX_LEN..PREFIX_LEN+LENGTH_LEN] == U32ToBigEndian(|a.BS|).value + && (|a.BS| == 0 ==> |ret.value| == PREFIX_LEN + LENGTH_LEN) + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=implication + //# String Sets MUST NOT contain duplicate entries. + // Other implications for Binary Sets apply here too + ensures a.SS? && ret.Success? ==> Seq.HasNoDuplicates(a.SS) + + ensures a.SS? && ret.Success? && !prefix ==> + && U32ToBigEndian(|a.SS|).Success? + && |ret.value| >= LENGTH_LEN + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|a.SS|).value + && BigEndianToU32(ret.value[0..LENGTH_LEN]).value == |a.SS| + ensures a.SS? && ret.Success? && prefix ==> + && U32ToBigEndian(|a.SS|).Success? + && |ret.value| >= PREFIX_LEN + LENGTH_LEN + && ret.value[0..TYPEID_LEN] == STRING_SET + && ret.value[PREFIX_LEN..PREFIX_LEN+LENGTH_LEN] == U32ToBigEndian(|a.SS|).value + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=implication + //# Number Sets MUST NOT contain duplicate entries. + // Other implications for Binary Sets apply here too + ensures a.NS? && ret.Success? ==> Seq.HasNoDuplicates(a.NS) + + ensures a.NS? && ret.Success? && !prefix ==> + && U32ToBigEndian(|a.NS|).Success? + && |ret.value| >= LENGTH_LEN + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|a.NS|).value + ensures a.NS? && ret.Success? && prefix ==> + && U32ToBigEndian(|a.NS|).Success? + && |ret.value| >= PREFIX_LEN + LENGTH_LEN + && ret.value[0..TYPEID_LEN] == NUMBER_SET + && ret.value[PREFIX_LEN..PREFIX_LEN+LENGTH_LEN] == U32ToBigEndian(|a.NS|).value + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list + //= type=implication + //# List MUST be serialized as: + //# | Field | Length | + //# | ------------- | -------- | + //# | List Count | 4 | + //# | List Entries | Variable | + ensures a.L? && ret.Success? && !prefix ==> + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-count + //= type=implication + //# List Count MUST be a big-endian unsigned integer + //# equal to the number of serialized list entries in + //# [List Entries](#list-entries). + && U32ToBigEndian(|a.L|).Success? + && |ret.value| >= LENGTH_LEN + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|a.L|).value + && BigEndianToU32(ret.value[0..LENGTH_LEN]).value == |a.L| + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-count + //= type=implication + //# List Count MAY be `0`, + //# in which case [List Entries](#list-entries) is an empty byte string. + && (|a.L| == 0 ==> |ret.value| == LENGTH_LEN) + + ensures a.L? && ret.Success? && prefix ==> + && U32ToBigEndian(|a.L|).Success? + && |ret.value| >= PREFIX_LEN + LENGTH_LEN + && ret.value[0..TYPEID_LEN] == LIST + && ret.value[PREFIX_LEN..PREFIX_LEN+LENGTH_LEN] == U32ToBigEndian(|a.L|).value + && (|a.L| == 0 ==> |ret.value| == PREFIX_LEN + LENGTH_LEN) + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#map-attribute + //= type=implication + //# Map MUST be serialized as: + //# | Field | Length | + //# | ----------------------- | -------- | + //# | Key Value Pair Count | 4 | + //# | Key Value Pair Entries | Variable | + ensures a.M? && ret.Success? && !prefix ==> + && U32ToBigEndian(|a.M|).Success? + && |ret.value| >= LENGTH_LEN + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-count + //= type=implication + //# Key Value Pair Count MUST be a big-endian unsigned integer + //# equal to the number of serialized key-value pairs in + //# [Key Value Pair Entries](#key-value-pair-entries). + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|a.M|).value + && BigEndianToU32(ret.value[0..LENGTH_LEN]).value == |a.M| + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-count + //= type=implication + //# Key Value Pair Count MAY be `0`, + //# in which case [Key Value Pair Entries](#key-value-pair-entries) is an empty bytestring. + && (|a.M| == 0 ==> |ret.value| == LENGTH_LEN) + + ensures a.M? && ret.Success? && prefix ==> + && U32ToBigEndian(|a.M|).Success? + && |ret.value| >= PREFIX_LEN + LENGTH_LEN + && ret.value[0..TYPEID_LEN] == AttrToTypeId(a) + && (|a.M| == 0 ==> |ret.value| == PREFIX_LEN + LENGTH_LEN) + + { + :- Need(depth <= MAX_STRUCTURE_DEPTH, "Depth of attribute structure to serialize exceeds limit of " + MAX_STRUCTURE_DEPTH_STR); + var baseBytes :- match a { + case S(s) => UTF8.Encode(s) + case N(n) => var nn :- Norm.NormalizeNumber(n); UTF8.Encode(nn) + case B(b) => Success(b) + case SS(ss) => StringSetAttrToBytes(ss) + case NS(ns) => NumberSetAttrToBytes(ns) + case BS(bs) => BinarySetAttrToBytes(bs) + case M(m) => MapAttrToBytes(a, m, depth) + case L(l) => ListAttrToBytes(l, depth) + case NULL(n) => Success([]) + case BOOL(b) => Success([BoolToUint8(b)]) + }; + if prefix then + var len :- U32ToBigEndian(|baseBytes|); + Success(AttrToTypeId(a) + len + baseBytes) + else + Success(baseBytes) + } + + function method StringSetAttrToBytes(ss: StringSetAttributeValue): (ret: Result, string>) + ensures ret.Success? ==> Seq.HasNoDuplicates(ss) + { + var asSet := Seq.ToSet(ss); + :- Need(|asSet| == |ss|, "String Set had duplicate values"); + Seq.LemmaNoDuplicatesCardinalityOfSet(ss); + + var count :- U32ToBigEndian(|ss|); + var body :- CollectString(ss); + Success(count + body) + } + + function method NumberSetAttrToBytes(ns: NumberSetAttributeValue): (ret: Result, string>) + ensures ret.Success? ==> Seq.HasNoDuplicates(ns) + { + var asSet := Seq.ToSet(ns); + :- Need(|asSet| == |ns|, "Number Set had duplicate values"); + Seq.LemmaNoDuplicatesCardinalityOfSet(ns); + + var count :- U32ToBigEndian(|ns|); + var body :- CollectString(ns); + Success(count + body) + } + + function method BinarySetAttrToBytes(bs: BinarySetAttributeValue): (ret: Result, string>) + ensures ret.Success? ==> Seq.HasNoDuplicates(bs) + { + var asSet := Seq.ToSet(bs); + :- Need(|asSet| == |bs|, "Binary Set had duplicate values"); + Seq.LemmaNoDuplicatesCardinalityOfSet(bs); + + var count :- U32ToBigEndian(|bs|); + var body :- CollectBinary(bs); + Success(count + body) + } + + // Specifying the parent (particularly, as the first parameter), + // along with the corresponding precondition, + // lets Dafny find the correct termination metric. + // See "The Parent Trick" for details: . + function method MapAttrToBytes(ghost parent: AttributeValue, m: MapAttributeValue, depth : nat): (ret: Result, string>) + requires forall kv <- m.Items :: kv.1 < parent + ensures MAX_MAP_SIZE < |m| ==> ret.Failure? + { + :- Need(|m| <= MAX_MAP_SIZE, "Map exceeds limit of " + MAX_MAP_SIZE_STR + " entries."); + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#value-type + //# Value Type MUST be the [Type ID](#type-id) of the type of [Map Value](#map-value). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#value-length + //# Value Length MUST be a big-endian unsigned integer + //# equal to the length of [Map Value](#map-value). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#map-value + //# Map Value MUST be a [Value](#value). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#map-value + //# A Map MAY hold any DynamoDB Attribute Value data type, + //# and MAY hold values of different types. + var bytesResults := map kv <- m.Items :: kv.0 := AttrToBytes(kv.1, true, depth+1); + var count :- U32ToBigEndian(|m|); + var bytes :- SimplifyMapValue(bytesResults); + var body :- CollectMap(bytes); + Success(count + body) + } + + function method ListAttrToBytes(l: ListAttributeValue, depth : nat): (ret: Result, string>) + ensures MAX_LIST_LENGTH < |l| ==> ret.Failure? + { + :- Need(|l| <= MAX_LIST_LENGTH, "List exceeds limit of " + MAX_LIST_LENGTH_STR + " entries."); + var count :- U32ToBigEndian(|l|); + var body :- CollectList(l, depth); + Success(count + body) + } + + lemma BigEndianLemma() + ensures U32ToBigEndian(3) == Success([0,0,0,3]) + ensures BigEndianToU32([0,0,0,3]) == Success(3) + {} + + lemma U32ToBigEndianRoundTrip(x : nat) + ensures U32ToBigEndian(x).Success? ==> + && BigEndianToU32(U32ToBigEndian(x).value).Success? + && BigEndianToU32(U32ToBigEndian(x).value).value == x + {} + + lemma BigEndianToU32RoundTrip(x : seq) + requires |x| == 4 + ensures BigEndianToU32(x).Success? ==> + && U32ToBigEndian(BigEndianToU32(x).value).Success? + && U32ToBigEndian(BigEndianToU32(x).value).value == x + {} + + function method U32ToBigEndian(x : nat) : (ret : Result, string>) + ensures ret.Success? ==> |ret.value| == LENGTH_LEN + { + if x > 0xffff_ffff then + Failure("Length was too big") + else + Success(UInt32ToSeq(x as uint32)) + } + + function method BigEndianToU32(x : seq) : (ret : Result) + { + if |x| < LENGTH_LEN then + Failure("Length of 4-byte integer was less than 4") + else + Success(SeqToUInt32(x[..LENGTH_LEN]) as nat) + } + + predicate IsSorted(s: seq, lessThanOrEq: (T, T) -> bool) { + forall j, k :: 0 <= j < k < |s| ==> lessThanOrEq(s[j], s[k]) + } + + function method EncodeString(s : string) : (ret : Result, string>) + // The Duvet implications set-entries and set-entry-length mentioned in SerializeBinaryValue + // are also implied here for String Sets and Number Sets + ensures ret.Success? ==> + && UTF8.Encode(s).Success? + && U32ToBigEndian(|UTF8.Encode(s).value|).Success? + && |ret.value| == LENGTH_LEN + |UTF8.Encode(s).value| + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|UTF8.Encode(s).value|).value + && ret.value[LENGTH_LEN..] == UTF8.Encode(s).value + { + var val :- UTF8.Encode(s); + var len :- U32ToBigEndian(|val|); + Success(len + val) + } + // String Set or Number Set to Bytes + function method {:tailrecursion} {:opaque} CollectString( + setToSerialize : StringSetAttributeValue, + serialized : seq := []) + : Result, string> + { + if |setToSerialize| == 0 then + Success(serialized) + else + var entry :- EncodeString(setToSerialize[0]); + CollectString(setToSerialize[1..], serialized + entry) + } + + + function method SerializeBinaryValue(b : BinaryAttributeValue) : (ret : Result, string>) + // for parallel implication for String Set and Number Set see EncodeString + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=implication + //# Each of these entries MUST be serialized as: + //# | Field | Length | + //# | ------------------- | ------------------------------------ | + //# | Set Entry Length | 4 | + //# | Set Entry Value | Variable. Equal to Set Entry Length. | + ensures ret.Success? ==> + && U32ToBigEndian(|b|).Success? + && |ret.value| == LENGTH_LEN + |b| + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entry-length + //= type=implication + //# Set Entry Length MUST be a big-endian unsigned integer + //# equal to the length of [Set Entry Value](#set-entry-value). + && ret.value[0..LENGTH_LEN] == U32ToBigEndian(|b|).value + && ret.value[LENGTH_LEN..] == b + { + var len :- U32ToBigEndian(|b|); + Success(len + b) + } + + // Binary Set to Bytes + function method {:tailrecursion} CollectBinary(setToSerialize : BinarySetAttributeValue, serialized : seq := []) : Result, string> + { + if |setToSerialize| == 0 then + Success(serialized) + else + var item :- SerializeBinaryValue(setToSerialize[0]); + CollectBinary(setToSerialize[1..], serialized + item) + } + + // List to Bytes + // Can't be {:tailrecursion} because it calls AttrToBytes which might again call CollectList + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-entries + //# Each list entry in the sequence MUST be serialized as: + //# | Field | Length | + //# | -------------------- | -------------------------- | + //# | List Entry Type | 2 | + //# | List Entry Length | 4 | + //# | List Entry Value | Variable. Equal to Length. | + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-entries + //# The order of these serialized list entries MUST match + //# the order of the entries in the original list. + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-entry-type + //# List Entry Type MUST be the [Type ID](#type-id) of the type of [List Entry Value](#list-entry-value). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-entry-length + //# List Entry Length MUST be a big-endian unsigned integer + //# equal to the length of [List Entry Value](#list-entry-value). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#list-entry-value + //# A List MAY hold any DynamoDB Attribute Value data type, + //# and MAY hold values of different types. + function method {:opaque} CollectList( + listToSerialize : ListAttributeValue, + depth : nat, + serialized : seq := [] + ) + : (ret : Result, string>) + ensures (ret.Success? && |listToSerialize| == 0) ==> (ret.value == serialized) + ensures (ret.Success? && |listToSerialize| == 0) ==> (|ret.value| == |serialized|) + { + if |listToSerialize| == 0 then + Success(serialized) + else + var val :- AttrToBytes(listToSerialize[0], true, depth+1); + CollectList(listToSerialize[1..], depth, serialized + val) + } + + function method SerializeMapItem(key : string, value : seq) : (ret : Result, string>) + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-type + //= type=implication + //# Key Type MUST be the [Type ID](#type-id) for Strings. + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#map-key + //= type=implication + //# Map Key MUST be a [String Value](#string). + ensures ret.Success? ==> + && |ret.value| >= TYPEID_LEN + && ret.value[0..TYPEID_LEN] == STRING + && UTF8.Encode(key).Success? + && |ret.value| == TYPEID_LEN + LENGTH_LEN + |UTF8.Encode(key).value| + |value| + && UTF8.Decode(ret.value[TYPEID_LEN+LENGTH_LEN..TYPEID_LEN+LENGTH_LEN+|UTF8.Encode(key).value|]).Success? + && UTF8.Decode(ret.value[TYPEID_LEN+LENGTH_LEN..TYPEID_LEN+LENGTH_LEN+|UTF8.Encode(key).value|]).value == key + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-length + //= type=implication + //# Key Length MUST be a non-zero big-endian unsigned integer + //# equal to the length of [Map Key](#map-key). + ensures ret.Success? ==> + && UTF8.Encode(key).Success? + && U32ToBigEndian(|UTF8.Encode(key).value|).Success? + && |ret.value| >= TYPEID_LEN+LENGTH_LEN + && ret.value[TYPEID_LEN..TYPEID_LEN+LENGTH_LEN] == U32ToBigEndian(|UTF8.Encode(key).value|).value + + { + var name :- UTF8.Encode(key); + assert UTF8.Decode(name).Success?; + var len :- U32ToBigEndian(|name|); + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-entries + //# Each key-value pair MUST be serialized as: + //# | Field | Length | + //# | ------------ | -------- | + //# | Key Type | 2 | + //# | Key Length | 4 | + //# | Map Key | Variable | + //# | Value Type | 2 | + //# | Value Length | 4 | + //# | Map Value | Variable | + + var serialized := STRING + len + name + value; + assert |serialized| == TYPEID_LEN + LENGTH_LEN + |name| + |value|; + Success(serialized) + } + // Map to Bytes + // input sequence is already serialized + function method {:tailrecursion} {:opaque} CollectMap( + mapToSerialize : map>, + serialized : seq := [] + ) + : (ret : Result, string>) + ensures (ret.Success? && |mapToSerialize| == 0) ==> (ret.value == serialized) + ensures (ret.Success? && |mapToSerialize| == 0) ==> (|ret.value| == |serialized|) + { + var keys := SortedSets.ComputeSetToOrderedSequence2(mapToSerialize.Keys, CharLess); + CollectOrderedMapSubset(keys, mapToSerialize, serialized) + } + + function method {:tailrecursion} {:opaque} CollectOrderedMapSubset( + keys : seq, + mapToSerialize : map>, + serialized : seq := [] + ) + : (ret : Result, string>) + requires forall k <- keys :: k in mapToSerialize + ensures (ret.Success? && |keys| == 0) ==> (ret.value == serialized) + ensures (ret.Success? && |keys| == 0) ==> (|ret.value| == |serialized|) + { + if |keys| == 0 then + Success(serialized) + else + var data :- SerializeMapItem(keys[0], mapToSerialize[keys[0]]); + CollectOrderedMapSubset(keys[1..], mapToSerialize, serialized + data) + } + + function method BoolToUint8(b : bool) : uint8 + { + if b then 1 else 0 + } + + // AttributeValue with number of bytes consumed in its construction + datatype AttrValueAndLength = AttrValueAndLength( + val : AttributeValue, + len : nat + ) + + predicate method IsUnique(s : seq) + { + |set x:T | x in s :: x| == |s| + } + + // Bytes to Binary Set + function method {:tailrecursion} {:vcs_split_on_every_assert} {:opaque} DeserializeBinarySet( + serialized : seq, + remainingCount : nat, + origSerializedSize : nat, + resultSet : AttrValueAndLength) + : (ret : Result) + requires resultSet.val.BS? + ensures ret.Success? ==> ret.value.val.BS? + requires |serialized| + resultSet.len == origSerializedSize + ensures ret.Success? ==> ret.value.len <= origSerializedSize + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#duplicates + //= type=implication + //# - Conversion from a Structured Data Binary Set MUST fail if it has duplicate values + ensures ret.Success? && (remainingCount == 0) ==> IsUnique(resultSet.val.BS) + { + if remainingCount == 0 then + :- Need(IsUnique(resultSet.val.BS), "Binary set values must not have duplicates"); + Success(resultSet) + else if |serialized| < LENGTH_LEN then + Failure("Out of bytes reading Binary Set") + else + var len :- BigEndianToU32(serialized); + var serialized := serialized[LENGTH_LEN..]; + if |serialized| < len as int then + Failure("Binary Set Structured Data has too few bytes") + else + var nattr := AttributeValue.BS(resultSet.val.BS + [serialized[..len]]); + DeserializeBinarySet(serialized[len..], remainingCount-1, origSerializedSize, AttrValueAndLength(nattr, resultSet.len + len + LENGTH_LEN)) + } + + // Bytes to String Set + function method {:tailrecursion} {:vcs_split_on_every_assert} {:opaque} DeserializeStringSet( + serialized : seq, + remainingCount : nat, + origSerializedSize : nat, + resultSet : AttrValueAndLength) + : (ret : Result) + requires resultSet.val.SS? + ensures ret.Success? ==> ret.value.val.SS? + requires |serialized| + resultSet.len == origSerializedSize + ensures ret.Success? ==> ret.value.len <= origSerializedSize + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#duplicates + //= type=implication + //# - Conversion from a Structured Data String Set MUST fail if it has duplicate values + ensures ret.Success? && (remainingCount == 0) ==> IsUnique(resultSet.val.SS) + { + if remainingCount == 0 then + :- Need(IsUnique(resultSet.val.SS), "String set values must not have duplicates"); + Success(resultSet) + else if |serialized| < LENGTH_LEN then + Failure("Out of bytes reading String Set") + else + var len :- BigEndianToU32(serialized); + var serialized := serialized[LENGTH_LEN..]; + if |serialized| < len as int then + Failure("String Set Structured Data has too few bytes") + else + var nstring :- UTF8.Decode(serialized[..len]); + var nattr := AttributeValue.SS(resultSet.val.SS + [nstring]); + DeserializeStringSet(serialized[len..], remainingCount-1, origSerializedSize, AttrValueAndLength(nattr, resultSet.len + len + LENGTH_LEN)) + } + + // Bytes to Number Set + function method {:tailrecursion} {:vcs_split_on_every_assert} {:opaque} DeserializeNumberSet( + serialized : seq, + remainingCount : nat, + origSerializedSize : nat, + resultSet : AttrValueAndLength) + : (ret : Result) + requires resultSet.val.NS? + ensures ret.Success? ==> ret.value.val.NS? + requires |serialized| + resultSet.len == origSerializedSize + ensures ret.Success? ==> ret.value.len <= origSerializedSize + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#duplicates + //= type=implication + //# - Conversion from a Structured Data Number Set MUST fail if it has duplicate values + ensures ret.Success? && (remainingCount == 0) ==> IsUnique(resultSet.val.NS) + { + if remainingCount == 0 then + :- Need(IsUnique(resultSet.val.NS), "Number set values must not have duplicates"); + Success(resultSet) + else if |serialized| < LENGTH_LEN then + Failure("Out of bytes reading String Set") + else + var len :- BigEndianToU32(serialized); + var serialized := serialized[LENGTH_LEN..]; + if |serialized| < len as int then + Failure("Number Set Structured Data has too few bytes") + else + var nstring :- UTF8.Decode(serialized[..len]); + var nattr := AttributeValue.NS(resultSet.val.NS + [nstring]); + DeserializeNumberSet(serialized[len..], remainingCount-1, origSerializedSize, AttrValueAndLength(nattr, resultSet.len + len + LENGTH_LEN)) + } + + // Bytes to List + // Can't be {:tailrecursion} because it calls BytesToAttr which might again call DeserializeList + function method {:vcs_split_on_every_assert} {:opaque} DeserializeList( + serialized : seq, + remainingCount : nat, + ghost origSerializedSize : nat, + depth : nat, + resultList : AttrValueAndLength) + : (ret : Result) + requires resultList.val.L? + requires remainingCount <= MAX_LIST_LENGTH + ensures ret.Success? ==> ret.value.val.L? + requires |serialized| + resultList.len == origSerializedSize + ensures ret.Success? ==> ret.value.len <= origSerializedSize + decreases |serialized| + { + if remainingCount == 0 then + Success(resultList) + else if |serialized| < 6 then + Failure("Out of bytes reading Type of List element") + else + var TerminalTypeId := serialized[0..2]; + var serialized := serialized[2..]; + var len :- BigEndianToU32(serialized); + var serialized := serialized[LENGTH_LEN..]; + if |serialized| < len then + Failure("Out of bytes reading Content of List element") + else + var nval :- BytesToAttr(serialized[..len], TerminalTypeId, false, depth+1); + var nattr := AttributeValue.L(resultList.val.L + [nval.val]); + DeserializeList(serialized[len..], remainingCount-1, origSerializedSize, depth, AttrValueAndLength(nattr, resultList.len + len + 6)) + } + + // Bytes to Map + // Can't be {:tailrecursion} because it calls BytesToAttr which might again call DeserializeMap + function method {:vcs_split_on_every_assert} {:opaque} DeserializeMap( + serialized : seq, + remainingCount : nat, + ghost origSerializedSize : nat, + depth : nat, + resultMap : AttrValueAndLength) + : (ret : Result) + requires resultMap.val.M? + requires remainingCount <= MAX_MAP_SIZE + ensures ret.Success? ==> ret.value.val.M? + requires |serialized| + resultMap.len == origSerializedSize + ensures ret.Success? ==> ret.value.len <= origSerializedSize + decreases |serialized| + { + ghost var serializedInitial := serialized; + + if remainingCount == 0 then + Success(resultMap) + else + // get typeId of key + :- Need(6 <= |serialized|, "Out of bytes reading Map Key"); + var TerminalTypeId_key := serialized[0..2]; + :- Need(TerminalTypeId_key == STRING, "Key of Map is not String"); + var serialized := serialized[2..]; + + // get key + var len :- BigEndianToU32(serialized); + var serialized := serialized[LENGTH_LEN..]; + :- Need(len as int <= |serialized|, "Key of Map of Structured Data has too few bytes"); + var key :- UTF8.Decode(serialized[..len]); + var serialized := serialized[len..]; + + assert |serialized| + 6 + len == |serializedInitial|; + + // get typeId of value + :- Need(2 <= |serialized|, "Out of bytes reading Map Value"); + :- Need(IsValid_AttributeName(key), "Key is not valid AttributeName"); + var TerminalTypeId_value := serialized[0..2]; + var serialized := serialized[2..]; + + // get value and construct result + var nval :- BytesToAttr(serialized, TerminalTypeId_value, true, depth+1); + var serialized := serialized[nval.len..]; + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-entries + //# This sequence MUST NOT contain duplicate [Map Keys](#map-key). + + //= specification/dynamodb-encryption-client/ddb-item-conversion.md#duplicates + //# - Conversion from a Structured Data Map MUST fail if it has duplicate keys + :- Need(key !in resultMap.val.M, "Duplicate key in map."); + var nattr := AttributeValue.M(resultMap.val.M[key := nval.val]); + var newResultMap := AttrValueAndLength(nattr, resultMap.len + nval.len + 8 + len); + assert |serialized| + newResultMap.len == origSerializedSize; + DeserializeMap(serialized, remainingCount - 1, origSerializedSize, depth, newResultMap) + } + + // Bytes to AttributeValue + // Can't be {:tailrecursion} because it calls DeserializeList and DeserializeMap which then call BytesToAttr + function method {:vcs_split_on_every_assert} {:opaque} BytesToAttr( + value : seq, + typeId : TerminalTypeId, + hasLen : bool, + depth : nat := 1 + ) + : (ret : Result) + ensures ret.Success? ==> ret.value.len <= |value| + ensures MAX_STRUCTURE_DEPTH < depth ==> ret.Failure? + decreases |value| + { + :- Need(depth <= MAX_STRUCTURE_DEPTH, "Depth of attribute structure to deserialize exceeds limit of " + MAX_STRUCTURE_DEPTH_STR); + var len :- if hasLen then + if |value| < LENGTH_LEN then + Failure("Out of bytes reading length") + else + BigEndianToU32(value) + else + Success(|value|); + var value := if hasLen then value[LENGTH_LEN..] else value; + var lengthBytes := if hasLen then LENGTH_LEN else 0; + + if |value| < len then + Failure("Structured Data has too few bytes") + + else if typeId == NULL then + if len != 0 then + Failure("NULL type did not have length zero") + else + // DynamoDB includes a boolean with the NULL + // We pick true because it appears that the server rejects NULL(false) + // NOTE : if you start with NULL(false), Decode(Encode()) will produce NULL(true) + // and so round-trip identity is NOT preserved + Success(AttrValueAndLength(AttributeValue.NULL(true), lengthBytes)) + + else if typeId == STRING then + var str :- UTF8.Decode(value[..len]); + Success(AttrValueAndLength(AttributeValue.S(str), len+lengthBytes)) + + else if typeId == NUMBER then + var str :- UTF8.Decode(value[..len]); + Success(AttrValueAndLength(AttributeValue.N(str), len+lengthBytes)) + + else if typeId == BINARY then + Success(AttrValueAndLength(AttributeValue.B(value[..len]), len+lengthBytes)) + + else if typeId == BOOLEAN then + if len != BOOL_LEN then + Failure("Boolean Structured Data has more than one byte") + else if value[0] == 0x00 then + Success(AttrValueAndLength(AttributeValue.BOOL(false), BOOL_LEN+lengthBytes)) + else if value[0] == 0x01 then + Success(AttrValueAndLength(AttributeValue.BOOL(true), BOOL_LEN+lengthBytes)) + else + Failure("Boolean Structured Data had inappropriate value") + + else if typeId == STRING_SET then + if |value| < LENGTH_LEN then + Failure("String Set Structured Data has less than LENGTH_LEN bytes") + else + var len :- BigEndianToU32(value); + var value := value[LENGTH_LEN..]; + DeserializeStringSet(value, len, |value| + LENGTH_LEN + lengthBytes, AttrValueAndLength(AttributeValue.SS([]), LENGTH_LEN+lengthBytes)) + + else if typeId == NUMBER_SET then + if |value| < LENGTH_LEN then + Failure("Number Set Structured Data has less than 4 bytes") + else + var len :- BigEndianToU32(value); + var value := value[LENGTH_LEN..]; + DeserializeNumberSet(value, len, |value| + LENGTH_LEN + lengthBytes, AttrValueAndLength(AttributeValue.NS([]), LENGTH_LEN + lengthBytes)) + + else if typeId == BINARY_SET then + if |value| < LENGTH_LEN then + Failure("Binary Set Structured Data has less than LENGTH_LEN bytes") + else + var len :- BigEndianToU32(value); + var value := value[LENGTH_LEN..]; + DeserializeBinarySet(value, len, |value| + LENGTH_LEN + lengthBytes, AttrValueAndLength(AttributeValue.BS([]), LENGTH_LEN + lengthBytes)) + + else if typeId == MAP then + if |value| < LENGTH_LEN then + Failure("List Structured Data has less than 4 bytes") + else + var len :- BigEndianToU32(value); + :- Need(len <= MAX_MAP_SIZE, "Map exceeds limit of " + MAX_MAP_SIZE_STR + " entries."); + var value := value[LENGTH_LEN..]; + DeserializeMap(value, len, |value| + LENGTH_LEN + lengthBytes, depth, AttrValueAndLength(AttributeValue.M(map[]), LENGTH_LEN + lengthBytes)) + + else if typeId == LIST then + if |value| < LENGTH_LEN then + Failure("List Structured Data has less than 4 bytes") + else + var len :- BigEndianToU32(value); + :- Need(len <= MAX_LIST_LENGTH, "List exceeds limit of " + MAX_LIST_LENGTH_STR + " entries."); + var value := value[LENGTH_LEN..]; + DeserializeList(value, len, |value| + LENGTH_LEN + lengthBytes, depth, AttrValueAndLength(AttributeValue.L([]), LENGTH_LEN + lengthBytes)) + + else + Failure("Unsupported TerminalTypeId") + + } + + function method FlattenValueMap(m : map>): map { + map k <- m | m[k].Success? :: k := m[k].value + } + + function method FlattenErrors(m : map>): set { + set k <- m | m[k].Failure? :: m[k].error + } + + lemma OneBadResult(m : map>) + requires ! forall k <- m :: m[k].Success? + ensures exists k <- m :: m[k].Failure? + ensures |FlattenErrors(m)| > 0 + { + assert exists k <- m :: m[k].Failure?; + var errors := FlattenErrors(m); + assert exists k :: k in m && m[k].Failure? && (m[k].error in errors); + } + + lemma MapKeysMatchItems(m : map) + ensures forall k :: k in m.Keys ==> (k, m[k]) in m.Items + {} + + lemma OneBadKey(s : map, bad : set, f : X -> bool) + requires !forall k <- s.Keys :: f(k) + requires bad == set k <- s.Keys | !f(k) :: k + ensures exists k <- s.Keys :: !f(k) + ensures |bad| > 0 + { + assert exists v :: v in bad && !f(v) && (v in bad); + } + + lemma SimplifyMapValueSuccess(m : map>) + ensures SimplifyMapValue(m).Success? <==> forall k <- m :: m[k].Success? + ensures SimplifyMapValue(m).Success? ==> forall kv <- m.Items :: kv.1.Success? + ensures SimplifyMapValue(m).Failure? <==> exists k : X | k in m.Keys :: m[k].Failure? + {} + + // Turn a map> into a Result, string> + // If anything reported Failure, return a Failure with all of the error messages + // + // useful when g in + // var ret := map kv <- m.Items | true :: kv.0 := g(kv.1); + // returns Result + function method SimplifyMapValue(m : map>) : (ret : Result, string>) + ensures ret.Success? ==> ret.value.Keys == m.Keys + ensures ret.Success? ==> |ret.value.Keys| == |m.Keys| + ensures ret.Success? ==> |ret.value| == |m| + { + if forall k <- m :: m[k].Success? then + var result := FlattenValueMap(m); + MapKeysMatchItems(m); + Success(result) + else + OneBadResult(m); + var badValues := FlattenErrors(m); + assert(|badValues| > 0); + var badValueSeq := SetToOrderedSequence(badValues, CharLess); + Failure(Join(badValueSeq, "\n")) + } +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations.dfy new file mode 100644 index 000000000..1829c3a3a --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations.dfy @@ -0,0 +1,166 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +include "../Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy" +include "../../../../DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy" +include "../../../../submodules/MaterialProviders/AwsCryptographicMaterialProviders/dafny/AwsCryptographicMaterialProviders/src/CMMs/RequiredEncryptionContextCMM.dfy" +include "AltDynamoToStruct.dfy" +include "AltDecryptStructure.dfy" + +module AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations refines AbstractAwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations { + import opened AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + import ComAmazonawsDynamodbTypes + import EU = DynamoDbItemEncryptorUtil + import CMP = AwsCryptographyMaterialProvidersTypes + import StructuredEncryption + import AltDynamoToStruct + import SortedSets + import Base64 + import opened StandardLibrary + import Seq + import CSE = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import SE = StructuredEncryptionUtil + import InternalLegacyOverride + import MaterialProviders + import RequiredEncryptionContextCMM + import SET = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import DDBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes + import DynamoDbEncryptionUtil + import StructuredEncryptionUtil + import StandardLibrary.String + import IE = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations + import IT = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import AltDecryptStructure + + datatype Config = Config( + nameonly inner : IE.Config + ) + + type InternalConfig = Config + type ValidConfig = x : Config | ValidInternalConfig?(x) witness * + + predicate ValidInternalConfig?(config: InternalConfig) + { + IE.ValidInternalConfig?(config.inner) + } + + function ModifiesInternalConfig(config: InternalConfig) : set + { + IE.ModifiesInternalConfig(config.inner) + } + + // string to Error + function method E(s : string) : Error { + DynamoDbPermuteDecryptorException(message := s) + } + + predicate PermuteDecryptEnsuresPublicly(input: PermuteDecryptInput, output: Result) + {true} + + // public Decrypt method + method {:vcs_split_on_every_assert} PermuteDecrypt(config: InternalConfig, input: PermuteDecryptInput) + returns (output: Result) + { + var realCount := |set k <- input.inner.encryptedItem | !(EU.ReservedPrefix <= k)|; + if realCount > EU.MAX_ATTRIBUTE_COUNT { + var actCount := String.Base10Int2String(realCount); + var maxCount := String.Base10Int2String(EU.MAX_ATTRIBUTE_COUNT); + return Failure(E("Item to decrypt had " + actCount + " attributes, but maximum allowed is " + maxCount)); + } + + :- Need( + && config.inner.partitionKeyName in input.inner.encryptedItem + && (config.inner.sortKeyName.None? || config.inner.sortKeyName.value in input.inner.encryptedItem) + , DynamoDbPermuteDecryptorException( message := IE.KeyMissingMsg(config.inner, input.inner.encryptedItem, "Decrypt"))); + + // Note: InternalLegacyOverride.DecryptItem checks that the legacy policy is correct. + if config.inner.internalLegacyOverride.Some? && config.inner.internalLegacyOverride.value.IsLegacyInput(input.inner) { + var decryptItemOutputR := config.inner.internalLegacyOverride.value.DecryptItem(input.inner); + var decryptItemOutput :- decryptItemOutputR.MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(e)); + //= specification/dynamodb-encryption-client/decrypt-item.md#behavior + //# The item returned by this operation MUST be the item outputted by the + //# [Legacy Encryptor](./ddb-table-encryption-config.md#legacy-encryptor). + return Success(PermuteDecryptOutput(inner := decryptItemOutput, didPermute := false)); + } + + if ( + && (|| config.inner.plaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ? + || config.inner.plaintextOverride.FORBID_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ?) + && IE.IsPlaintextItem(input.inner.encryptedItem) + ) { + var passthroughOutput := IT.DecryptItemOutput( + plaintextItem := input.inner.encryptedItem, + parsedHeader := None + ); + return Success(PermuteDecryptOutput(inner := passthroughOutput, didPermute := false)); + } + + :- Need(!IE.IsPlaintextItem(input.inner.encryptedItem), + DynamoDbPermuteDecryptorException( + message := "Encrypted item missing expected header and footer attributes")); + + var encryptedStructure :- AltDynamoToStruct.ItemToStructured(input.inner.encryptedItem) + .MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDb(e)); + var context :- IE.MakeEncryptionContext(config.inner, encryptedStructure).MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(e)); + var authenticateSchema := IE.ConfigToAuthenticateSchema(config.inner, input.inner.encryptedItem); + var wrappedStruct := CSE.StructuredData( + content := CSE.StructuredDataContent.DataMap(encryptedStructure), + attributes := None); + + //= specification/dynamodb-encryption-client/decrypt-item.md#behavior + //# This operation MUST create a + //# [Required Encryption Context CMM](https://github.com/awslabs/private-aws-encryption-sdk-specification-staging/blob/dafny-verified/framework/required-encryption-context-cmm.md) + //# with the following inputs: + //# - This item encryptor's [CMM](./ddb-table-encryption-config.md#cmm) as the underlying CMM. + //# - The keys from the [DynamoDB Item Base Context](./encrypt-item.md#dynamodb-item-base-context). + + var reqCMMR := config.inner.cmpClient.CreateRequiredEncryptionContextCMM( + CMP.CreateRequiredEncryptionContextCMMInput( + underlyingCMM := Some(config.inner.cmm), + keyring := None, + requiredEncryptionContextKeys := SortedSets.ComputeSetToOrderedSequence2(context.Keys, StructuredEncryptionUtil.ByteLess) + ) + ); + var reqCMM :- reqCMMR.MapFailure(e => AwsCryptographyMaterialProviders(e)); + + var decryptVal :- AltDecryptStructure.AltDecryptStructure( + config.inner.structuredEncryption.config, + CSE.DecryptStructureInput( + tableName := config.inner.logicalTableName, + encryptedStructure := wrappedStruct, + authenticateSchema := authenticateSchema, + //= specification/dynamodb-encryption-client/decrypt-item.md#behavior + //# - CMM MUST be the CMM constructed above. + cmm:=reqCMM, + encryptionContext:=Some(context) + ), + input.maxSetSize as nat + ); + + var decryptedData := decryptVal.inner.plaintextStructure; + var ddbItem :- AltDynamoToStruct.StructuredToItem(decryptedData.content.DataMap) + .MapFailure(e => Error.AwsCryptographyDbEncryptionSdkDynamoDb(e)); + + var schemaToConvert := decryptVal.inner.parsedHeader.cryptoSchema; + + var parsedAuthActions :- IE.ConvertCryptoSchemaToAttributeActions(config.inner, schemaToConvert) + .MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(e)); + + var parsedHeader := IT.ParsedHeader( + attributeActionsOnEncrypt := parsedAuthActions, + algorithmSuiteId := decryptVal.inner.parsedHeader.algorithmSuiteId, + storedEncryptionContext := decryptVal.inner.parsedHeader.storedEncryptionContext, + encryptedDataKeys := decryptVal.inner.parsedHeader.encryptedDataKeys + ); + + var result := IT.DecryptItemOutput( + plaintextItem := ddbItem, + parsedHeader := Some(parsedHeader) + ); + + output := Success( + PermuteDecryptOutput( + inner := result, + didPermute := decryptVal.didPermute + )); + } +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/Counter.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/Counter.dfy new file mode 100644 index 000000000..f3a42e8d2 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/Counter.dfy @@ -0,0 +1,123 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +module {:options "-functionSyntax:4"} CounterClass { + function Multiply(xs: seq): nat + { + if |xs| == 0 then 1 + else xs[0] * Multiply(xs[1..]) + } + + class Counter { + var sizes : seq + var curr : array + var remaining : nat + + predicate Valid() + reads this, this.curr + { + && 0 < |sizes| + && |sizes| == curr.Length + && (forall i | 0 <= i < |sizes| :: 0 < sizes[i]) + && (forall i | 0 <= i < |sizes| :: curr[i] < sizes[i]) + } + + constructor(s : seq) + requires 0 < |s| + requires forall i | 0 <= i < |s| :: 0 < s[i] + ensures |sizes| == |s| + ensures Valid() + ensures fresh(curr) + ensures remaining == Multiply(s) + { + sizes := s; + curr := new nat[|s|](i => 0); + remaining := 0; + new; + remaining := Multiply(s); + } + + function at(pos : nat) : nat + requires pos < |sizes| + requires Valid() + ensures Valid() + reads this, this.curr + ensures at(pos) < this.sizes[pos] + { + this.curr[pos] + } + + method next() returns (output : nat) + requires Valid() + ensures Valid() + modifies this`remaining, this.curr + ensures this.curr == old(this.curr) + ensures 0 <= output <= |sizes| + ensures sizes == old(sizes) + ensures output != 0 ==> (forall i | output < i < |sizes| :: curr[i] == old(curr[i])) + requires 0 < remaining + ensures remaining == old(remaining) - 1 + { + remaining := remaining - 1; + if remaining == 0 { + return 0; + } + for i := 0 to |sizes| + invariant Valid() + invariant forall j | i < j < |sizes| :: curr[j] == old(curr[j]) + invariant remaining == old(remaining) - 1 + { + if (curr[i]+1) < sizes[i] { + curr[i] := curr[i] + 1; + return i+1; + } else { + curr[i] := 0; + } + } + return 0; + } + } + + method {:test} TestCounter111() { + var c := new Counter([1,1,1]); + expect c.at(0) == 0; + expect c.at(1) == 0; + expect c.at(2) == 0; + var n := c.next(); + expect n == 0; + } + + method {:test} TestCounter23() { + var c := new Counter([2,3]); + expect c.at(0) == 0; + expect c.at(1) == 0; + + var n := c.next(); + expect n == 1; + expect c.at(0) == 1; + expect c.at(1) == 0; + + n := c.next(); + expect n == 2; + expect c.at(0) == 0; + expect c.at(1) == 1; + + n := c.next(); + expect n == 1; + expect c.at(0) == 1; + expect c.at(1) == 1; + + n := c.next(); + expect n == 2; + expect c.at(0) == 0; + expect c.at(1) == 2; + + n := c.next(); + expect n == 1; + expect c.at(0) == 1; + expect c.at(1) == 2; + + n := c.next(); + expect n == 0; + } +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/Index.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/Index.dfy new file mode 100644 index 000000000..e375c0796 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/Index.dfy @@ -0,0 +1,65 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +include "AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations.dfy" +include "AltDecryptStructure.dfy" +include "AltDynamoToStruct.dfy" +include "Permute.dfy" +include "ValidatePermuted.dfy" + +module + {:extern "software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny" } + DynamoDbPermuteDecryptor refines AbstractAwsCryptographyDbEncryptionSdkDecryptWithPermuteService +{ + import opened DynamoDbItemEncryptorUtil + import StructuredEncryption + import CSE = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import DDBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes + import MaterialProviders + import Operations = AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations + import SE = StructuredEncryptionUtil + import InternalLegacyOverride + import SortedSets + import DDB = ComAmazonawsDynamodbTypes + import DynamoDbItemEncryptor + + + // There is no sensible default, so construct something simple but invalid at runtime. + function method DefaultDynamoDbPermuteDecryptorConfig(): DynamoDbPermuteDecryptorConfig + { + DynamoDbPermuteDecryptorConfig( + inner := DynamoDbItemEncryptor.DefaultDynamoDbItemEncryptorConfig() + ) + } + + method {:vcs_split_on_every_assert} DynamoDbPermuteDecryptor(config: DynamoDbPermuteDecryptorConfig) + returns (res: Result) + { + var innerClientR := DynamoDbItemEncryptor.DynamoDbItemEncryptor(config.inner); + var innerClient :- innerClientR.MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(e)); + var internalConfig := Operations.Config( + inner := innerClient.config + ); + var client := new DynamoDbPermuteDecryptorClient(internalConfig); + return Success(client); + } + + class DynamoDbPermuteDecryptorClient... { + + predicate ValidState() + { + && Operations.ValidInternalConfig?(config) + && History !in Operations.ModifiesInternalConfig(config) + && Modifies == Operations.ModifiesInternalConfig(config) + {History} + } + + constructor(config: Operations.InternalConfig) + { + this.config := config; + History := new IDynamoDbPermuteDecryptorClientCallHistory(); + Modifies := Operations.ModifiesInternalConfig(config) + {History}; + } + + } + +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/Permute.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/Permute.dfy new file mode 100644 index 000000000..f11e6c3f7 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/Permute.dfy @@ -0,0 +1,107 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +module {:options "-functionSyntax:4"} Permutations { + + function factorial(n : nat) : nat + ensures 0 < n ==> 0 < factorial(n) + { + if n == 0 then + 0 + else if n == 1 then + 1 + else + n * factorial(n-1) + } + + method GeneratePermutations(source : seq) returns (result : seq>) + ensures forall x <- result :: multiset(source) == multiset(x) + ensures |result| == factorial(|source|) + ensures forall x <- result :: (forall e <- x :: e in source) + { + if |source| == 0 { + return []; + } + if |source| == 1 { + return [source]; + } + var A := new T[|source|](i requires 0 <= i < |source| => source[i]); + assert A[..] == source; + result := Permute(A.Length, A, source); + } + + method Swap(A : array, x : nat, y : nat) + requires 0 <= x < A.Length + requires 0 <= y < A.Length + modifies A + ensures multiset(A[..]) == multiset(old(A[..])) + ensures forall e <- A[..] :: e in old(A[..]) + ensures A[x] == old(A[y]) + ensures A[y] == old(A[x]) + { + var tmp := A[x]; + A[x] := A[y]; + A[y] := tmp; + } + + // https://en.wikipedia.org/wiki/Heap%27s_algorithm + // Each step generates the k! permutations that end with the same n-k final elements + method Permute(k : nat, A : array, ghost source : seq) returns (result : seq>) + requires 0 < k <= A.Length + modifies A + requires forall x <- A[..] :: x in source + requires multiset(source) == multiset(A[..]) + ensures multiset(source) == multiset(A[..]) + ensures forall x <- result :: multiset(source) == multiset(x) + ensures forall x <- result :: (forall e <- x :: e in source) + ensures |result| == factorial(k) + ensures forall x <- A[..] :: x in source + { + if k == 1 { + assert forall x <- A[..] :: x in source; + return [A[..]]; + } else { + var result : seq> := []; + for i := 0 to k + invariant forall x <- result :: multiset(source) == multiset(x) + invariant multiset(old(A[..])) == multiset(A[..]) + invariant multiset(source) == multiset(A[..]) + invariant forall x <- result :: multiset(A[..]) == multiset(x) + invariant |result| == factorial(k-1) * i + invariant forall x <- result :: (forall e <- x :: e in source) + invariant forall x <- A[..] :: x in source + { + var next := Permute(k - 1, A, source); + result := result + next; + if (k % 2) == 0 { + Swap(A, i, k-1); + } else { + Swap(A, 0, k-1); + } + } + return result; + } + } + + method {:test} BasicTests() { + var zero := GeneratePermutations([]); + var one := GeneratePermutations([1]); + var two := GeneratePermutations([1,2]); + var three := GeneratePermutations([1,2,3]); + var four := GeneratePermutations([1,2,3,4]); + expect zero == []; + expect one == [[1]]; + expect two == [[1,2],[2,1]]; + expect |two| == |multiset(two)|; + expect |three| == |multiset(three)|; + expect |four| == |multiset(four)|; + expect three == [[1, 2, 3], [2, 1, 3], [3, 1, 2], [1, 3, 2], [2, 3, 1], [3, 2, 1]]; + expect four == [ + [1, 2, 3, 4], [2, 1, 3, 4], [3, 1, 2, 4], [1, 3, 2, 4], [2, 3, 1, 4], [3, 2, 1, 4], + [4, 2, 3, 1], [2, 4, 3, 1], [3, 4, 2, 1], [4, 3, 2, 1], [2, 3, 4, 1], [3, 2, 4, 1], + [4, 1, 3, 2], [1, 4, 3, 2], [3, 4, 1, 2], [4, 3, 1, 2], [1, 3, 4, 2], [3, 1, 4, 2], + [4, 1, 2, 3], [1, 4, 2, 3], [2, 4, 1, 3], [4, 2, 1, 3], [1, 2, 4, 3], [2, 1, 4, 3] + ]; + } +} + diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/src/ValidatePermuted.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/src/ValidatePermuted.dfy new file mode 100644 index 000000000..62eed1865 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/src/ValidatePermuted.dfy @@ -0,0 +1,345 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +include "../Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/Model/AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.dfy" +include "../../../../DynamoDbEncryption/dafny/StructuredEncryption/src/Footer.dfy" +include "Permute.dfy" +include "Counter.dfy" + +module {:options "-functionSyntax:4"} ValidatePermuted { + import opened Wrappers + import opened StandardLibrary + import opened StandardLibrary.UInt + import opened AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + import SET = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import SE = StructuredEncryptionUtil + import opened StructuredEncryptionFooter + import opened StandardLibrary.String + import Permutations + import CounterClass + + const SET_T : uint8 := 0x01 + const STRING_T : uint8 := 0x01 + const NUMBER_T : uint8 := 0x02 + const BINARY_T : uint8 := 0xFF + const MAP_T : uint8 := 0x02 + const LIST_T : uint8 := 0x03 + const NULL_T : uint8 := 0x00 + + const STRING_SET : SET.TerminalTypeId := [SET_T, STRING_T] + const NUMBER_SET : SET.TerminalTypeId := [SET_T, NUMBER_T] + const BINARY_SET : SET.TerminalTypeId := [SET_T, BINARY_T] + const MAP : SET.TerminalTypeId := [MAP_T, NULL_T] + const LIST : SET.TerminalTypeId := [LIST_T, NULL_T] + + const TYPEID_LEN : nat := 2 // number of bytes in a TerminalTypeId + const LENGTH_LEN : nat := 4 // number of bytes in an encoded count or length + const PREFIX_LEN : nat := 6 // number of bytes in a prefix, i.e. 2-byte type and 4-byte length + + // string to Error + function E(s : string) : Error { + DynamoDbPermuteDecryptorException(message := s) + } + + method Validate( + footer : Footer, + client: Primitives.AtomicPrimitivesClient, + mat : CMP.DecryptionMaterials, + edks : CMP.EncryptedDataKeyList, + signedFields : seq, + encFields : seq, + encData : SE.StructuredDataCanon, + allData : SE.StructuredDataCanon, + header : SE.Bytes, + maxSetSize : nat) + returns (ret : Result) + requires Materials.DecryptionMaterialsWithPlaintextDataKey(mat) + requires SE.ValidSuite(mat.algorithmSuite) + requires Header.ValidEncryptionContext(mat.encryptionContext) + requires forall k <- signedFields :: k in allData + requires forall k <- encFields :: k in allData + requires forall k <- encData :: encData[k].content.Terminal? + requires forall k <- allData :: allData[k].content.Terminal? + + modifies client.Modifies + requires client.ValidState() + ensures client.ValidState() + { + var sign_only := Seq.Filter(n => n !in encFields, signedFields); + reveal Seq.Filter(); + expect forall k <- sign_only :: k in signedFields; + var sets : seq := FindSets(sign_only, allData); + if |sets| == 0 { + return Failure(E("Validation failed, and no sets were present.")); + } + // FIXME -- maximum set size? + var allMembers :- GetAllMembers(sets, allData); + assert |allMembers| == |sets|; + for i := 0 to |sets| { + if maxSetSize < |allMembers[i]| { + return Failure(E("maxSetSize was " + String.Base10Int2String(maxSetSize) + " but item holds a set of size " + String.Base10Int2String(|allMembers[i]|) + ".")); + } + } + + var allPerms := GetAllPermutations(allMembers); + assert |allPerms| == |sets|; + var allSizes := GetAllSizes(allPerms); + assert |allSizes| == |sets|; + var counter := new CounterClass.Counter(allSizes); + ghost var origCounter := counter; + assert |counter.sizes| == |sets|; + var newMap : SE.StructuredDataCanon := allData; + var n : nat := |sets|; + while 0 < counter.remaining && 0 < n + decreases counter.remaining + invariant counter.Valid() + invariant forall k <- signedFields :: k in newMap + invariant forall k <- encFields :: k in newMap + invariant counter == origCounter + invariant fresh(counter) + invariant fresh(counter.curr) + invariant counter.sizes == origCounter.sizes + invariant |counter.sizes| == |sets| + invariant n <= |sets| + { + ghost var oldRemaining := counter.remaining; + assert |counter.sizes| == |sets|; + for i := 0 to n + invariant forall k <- signedFields :: k in newMap + invariant forall k <- encFields :: k in newMap + invariant counter.Valid() + invariant counter == origCounter + invariant fresh(counter) + invariant fresh(counter.curr) + invariant |counter.sizes| == |sets| + invariant 0 < counter.remaining + invariant 0 < n + invariant oldRemaining == counter.remaining + { + var data : SE.Bytes := allData[sets[i].name].content.Terminal.value; + var typeId := allData[sets[i].name].content.Terminal.typeId; + expect counter.at(i) < |allPerms[i]|; + expect forall s : SetMember <- allPerms[i][counter.at(i)] :: s.start <= s.end; + var newSet := sets[i].MakeSet(data, allPerms[i][counter.at(i)]); + expect sets[i].start <= |data|; + expect sets[i].end <= |data|; + var newData := data[..sets[i].start] + newSet + data[sets[i].end..]; + newMap := newMap[sets[i].name := SE.ValueToData(value := newData, typeId := typeId)]; + } + var result := footer.validate(client, mat, edks, signedFields, encFields, encData, newMap, header); + if result.Success? { + return Success(newMap); + } + n := counter.next(); + } + return Failure(E("Validation failed after permuting " + String.Base10Int2String(|sets|) + " sets.")); + } + + method GetAllMembers(sets : seq, allData : SE.StructuredDataCanon) + returns (output : Result>, Error>) + requires forall s <- sets :: s.name in allData + ensures output.Success? ==> |output.value| == |sets| + ensures output.Success? ==> forall s <- output.value :: 0 < |s| + { + var result : seq> := []; + for i := 0 to |sets| + invariant forall s <- result :: 0 < |s| + invariant |result| == i + { + var membersR := sets[i].GetMembers(allData); + var members :- membersR.MapFailure(e => E(e)); + expect 0 < |members|; + result := result + [members]; + } + return Success(result); + } + + method GetAllPermutations(sets : seq>) + returns (output : seq>>) + requires forall s <- sets :: 0 < |s| + ensures |output| == |sets| + ensures forall i | 0 <= i < |sets| :: |output[i]| == Permutations.factorial(|sets[i]|) + ensures forall i | 0 <= i < |sets| :: 0 < |output[i]| + { + var result : seq>> := []; + for i := 0 to |sets| + invariant |result| == i + invariant forall j | 0 <= j < i :: |result[j]| == Permutations.factorial(|sets[j]|) + invariant forall j | 0 <= j < i :: 0 < |result[j]| + { + var perms : seq> := Permutations.GeneratePermutations(sets[i]); + assert 0 < |sets[i]|; + assert |perms| == Permutations.factorial(|sets[i]|); + assert 0 < |perms|; + result := result + [perms]; + } + return result; + } + + method GetAllSizes(sets : seq>>) + returns (output : seq) + ensures |output| == |sets| + ensures forall i | 0 <= i < |sets| :: output[i] == |sets[i]| + { + var result : seq := []; + for i := 0 to |sets| + invariant |result| == i + invariant forall j | 0 <= j < i :: result[j] == |sets[j]| + { + result := result + [|sets[i]|]; + } + return result; + } + + + datatype SetMember = SetMember ( + start : nat, + end : nat + ) + + + datatype OneSet = OneSet ( + name : SE.CanonicalPath, + start : nat, + end : nat + ) + { + method GetMembers(allData : SE.StructuredDataCanon) returns (output : Result, string>) + requires name in allData + ensures output.Success? ==> + && (forall s <- output.value :: s.start <= s.end) + { + var data : SE.Bytes := allData[name].content.Terminal.value; + var offset : nat := start; + expect offset + 4 < |data|; + var count :- BigEndianToU32(data[offset..]); + offset := offset + 4; + var result : seq := []; + for i := 0 to count + invariant forall s <- result :: s.start <= s.end + { + if offset > |data| { + return Failure("Unexpected end of Set"); + } + var length :- BigEndianToU32(data[offset..]); + length := length + LENGTH_LEN; + result := result + [SetMember(offset, offset+length)]; + offset := offset + length; + } + return Success(result); + } + method MakeSet(data : SE.Bytes, perm : seq) returns (output : SE.Bytes) + requires forall s <- perm :: s.start <= s.end + { + expect start+4 <= |data|; + output := data[start..start+4]; + for i := 0 to |perm| { + expect perm[i].start <= |data|; + expect perm[i].end <= |data|; + output := output + data[perm[i].start..perm[i].end]; + } + return output; + } + } + + predicate IsSetType(t : SET.TerminalTypeId) + { + || t == STRING_SET + || t == NUMBER_SET + || t == BINARY_SET + } + + method FindSets3(fieldName : SE.CanonicalPath, attrType : SET.TerminalTypeId, attrLen : nat, value : SE.Bytes, offset : nat) returns (output : seq) + ensures forall s <- output :: s.name == fieldName + decreases |value| - offset + { + expect offset <= |value|; + if IsSetType(attrType) { + return [OneSet(fieldName, offset, offset+attrLen)]; + } else if attrType == LIST { + if |value| < offset + 6 { + return []; + } + output := []; + var off := offset; + var count :- expect BigEndianToU32(value[off..]); + off := off + LENGTH_LEN; + for i := 0 to count + invariant off > offset + invariant forall s <- output :: s.name == fieldName + { + if |value| < off + 6 { + return []; + } + var innerType := value[off..off+2]; + var innerLen :- expect BigEndianToU32(value[off+TYPEID_LEN..]); + var newSets := FindSets3(fieldName, innerType, innerLen, value, off+PREFIX_LEN); + output := output + newSets; + off := off + innerLen + PREFIX_LEN; + } + } else if attrType == MAP { + if |value| < offset + 6 { + return []; + } + output := []; + var off := offset; + var count :- expect BigEndianToU32(value[off..]); + off := off + LENGTH_LEN; + for i := 0 to count + invariant off > offset + invariant forall s <- output :: s.name == fieldName + { + if |value| < off + 6 {return [];} + var keyType := value[off..off+2]; + var keyLen :- expect BigEndianToU32(value[off+TYPEID_LEN..]); + off := off + PREFIX_LEN + keyLen; + + if |value| < off + 6 {return [];} + var innerType := value[off..off+2]; + var innerLen :- expect BigEndianToU32(value[off+TYPEID_LEN..]); + var newSets := FindSets3(fieldName, innerType, innerLen, value, off+PREFIX_LEN); + output := output + newSets; + off := off + innerLen + PREFIX_LEN; + } + } else { + return []; + } + } + + function BigEndianToU32(x : seq) : (ret : Result) + { + if |x| < LENGTH_LEN then + Failure("Length of 4-byte integer was less than 4") + else + Success(SeqToUInt32(x[..LENGTH_LEN]) as nat) + } + + method FindSets2(fieldName : SE.CanonicalPath, fieldData : SE.StructuredDataTerminalType) returns (output : seq) + ensures forall s <- output :: s.name == fieldName + { + if IsSetType(fieldData.content.Terminal.typeId) { + return [OneSet(fieldName, 0, |fieldData.content.Terminal.value|)]; + } else if fieldData.content.Terminal.typeId == MAP { + output := FindSets3(fieldName, MAP, |fieldData.content.Terminal.value|, fieldData.content.Terminal.value, 0); + } else if fieldData.content.Terminal.typeId == LIST { + output := FindSets3(fieldName, LIST, |fieldData.content.Terminal.value|, fieldData.content.Terminal.value, 0); + } else { + return []; + } + } + + method FindSets(fieldNames : seq, fieldData : SE.StructuredDataCanon) returns (output : seq) + requires forall k <- fieldNames :: k in fieldData + ensures forall s <- output :: s.name in fieldData + { + output := []; + for i := 0 to |fieldNames| + invariant forall s <- output :: s.name in fieldData + { + var newSets := FindSets2(fieldNames[i], fieldData[fieldNames[i]]); + assert forall s <- newSets :: s.name == fieldNames[i]; + output := output + newSets; + } + } +} \ No newline at end of file diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/test/MakeSets.not_dfy b/DecryptWithPermute/dafny/DecryptWithPermute/test/MakeSets.not_dfy new file mode 100644 index 000000000..c2d9c865d --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/test/MakeSets.not_dfy @@ -0,0 +1,488 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + + + +/* + +=================================================================================== + +This test was run with the faulty DB-ESDK 3.1 package, +which generated some items which could be decrypted +(tested with GoodGet below) +which generated and some items which could not be decrypted +(tested with BadGet below) + +These items were all written to the `SetTests` table, +where they are successfully decrypted with the AltDecryptItem +from provided in this DecryptWithPermute service. + +This test MUST NOT be run with newer correctly functioning versions +of the DB_ESDK, lest these items be overwritten with normal undamaged versions. + +=================================================================================== + +*/ + + + + +include "../../DynamoDbEncryption/src/Index.dfy" +include "../../DynamoDbEncryptionTransforms/test/TestFixtures.dfy" + +module MakeSetsTest { + import opened Wrappers + import opened StandardLibrary.UInt + import opened DynamoDbItemEncryptorUtil + import opened AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes + import opened AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import opened AwsCryptographyDbEncryptionSdkDynamoDbTypes + + import MaterialProviders + import opened DynamoDbItemEncryptor + import AwsCryptographyMaterialProvidersTypes + import Types = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import UTF8 + import DDB = ComAmazonawsDynamodbTypes + import TestFixtures + import AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations + import CSE = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import SE = StructuredEncryptionUtil + import DDBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes + import AlgorithmSuites + import StandardLibrary.String + import opened Com.Amazonaws.Dynamodb + + // round trip + // encrypt => encrypted fields changed, others did not + // various errors + + function method DDBS(x : string) : DDB.AttributeValue { + DDB.AttributeValue.S(x) + } + + method GetEncryptorConfig() returns (output : DynamoDbItemEncryptorConfig) { + var keyring := TestFixtures.GetKmsKeyring(); + var logicalTableName := TestFixtures.GetTableName("SetTests"); + output := DynamoDbItemEncryptorConfig( + logicalTableName := logicalTableName, + partitionKeyName := "Name", + sortKeyName := None(), + attributeActionsOnEncrypt := map[ + "Name" := CSE.SIGN_ONLY, + "Encrypted" := CSE.ENCRYPT_AND_SIGN, + "TheSet" := CSE.SIGN_ONLY, + "Set2" := CSE.SIGN_ONLY, + "Set3" := CSE.SIGN_ONLY, + "TheMap" := CSE.SIGN_ONLY, + "TheList" := CSE.SIGN_ONLY + ], + allowedUnsignedAttributes := None(), + allowedUnsignedAttributePrefix := None(), + keyring := Some(keyring), + cmm := None(), + algorithmSuiteId := None(), + plaintextOverride := None(), + legacyOverride := None() + ); + } + + method GetDynamoDbItemEncryptor() + returns (encryptor: DynamoDbItemEncryptorClient) + ensures encryptor.ValidState() + ensures fresh(encryptor) + ensures fresh(encryptor.Modifies) + { + var config := GetEncryptorConfig(); + encryptor := TestFixtures.GetDynamoDbItemEncryptorFrom(config); + } + + function method MakePutInput(item : DDB.PutItemInputAttributeMap) : DDB.PutItemInput + { + DDB.PutItemInput( + TableName := "SetTests", + Item := item, + Expected := None, + ReturnValues := None, + ReturnConsumedCapacity := None, + ReturnItemCollectionMetrics := None, + ConditionalOperator := None, + ConditionExpression := None, + ExpressionAttributeNames := None, + ExpressionAttributeValues := None + ) + } + + function method MakeGetInput(name : string) : DDB.GetItemInput + { + DDB.GetItemInput( + TableName := "SetTests", + Key := map["Name" := DDB.AttributeValue.S(name)], + AttributesToGet := None, + ConsistentRead := Some(true), + ReturnConsumedCapacity := None, + ProjectionExpression := None, + ExpressionAttributeNames := None + ) + } + + method DoPutItemSS( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbItemEncryptorClient, + setParts : map, + tag : string, + parts : seq + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies + { + var name : string := tag; + var setVal : seq := []; + for i := 0 to |parts| { + expect parts[i] in setParts; + name := name + parts[i]; + setVal := setVal + [setParts[parts[i]]]; + } + + var inputItem : DDB.AttributeMap := map[ + "Name" := DDB.AttributeValue.S(name), + "TheSet" := DDB.AttributeValue.SS(setVal) + ]; + + var encryptRes :- expect encryptor.EncryptItem( + Types.EncryptItemInput( + plaintextItem:=inputItem + ) + ); + + var resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + } + + method DoPutItemNS( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbItemEncryptorClient, + setParts : map, + tag : string, + parts : seq + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies + { + var name : string := tag; + var setVal : seq := []; + for i := 0 to |parts| { + expect parts[i] in setParts; + name := name + parts[i]; + setVal := setVal + [setParts[parts[i]]]; + } + + var inputItem : DDB.AttributeMap := map[ + "Name" := DDB.AttributeValue.S(name), + "TheSet" := DDB.AttributeValue.NS(setVal) + ]; + + var encryptRes :- expect encryptor.EncryptItem( + Types.EncryptItemInput( + plaintextItem:=inputItem + ) + ); + + var resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + } + + method DoPutItemBS( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbItemEncryptorClient, + setParts : map>, + tag : string, + parts : seq + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies + { + var name : string := tag; + var setVal : seq> := []; + for i := 0 to |parts| { + expect parts[i] in setParts; + name := name + parts[i]; + setVal := setVal + [setParts[parts[i]]]; + } + + var inputItem : DDB.AttributeMap := map[ + "Name" := DDB.AttributeValue.S(name), + "TheSet" := DDB.AttributeValue.BS(setVal) + ]; + + var encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + var resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + } + + method GoodGet( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbItemEncryptorClient, + name : string + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies +{ + var resp :- expect client.GetItem(MakeGetInput(name)); + expect resp.Item.Some?; + + var decryptRes := encryptor.DecryptItem( + Types.DecryptItemInput( + encryptedItem:=resp.Item.value + ) + ); + + if decryptRes.Failure? { + print("Accidental Failure with " + name + "\n"); + print "\n\n",decryptRes,"\n\n"; + } + expect decryptRes.Success?; +} + + method BadGet( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbItemEncryptorClient, + name : string + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies +{ + var resp :- expect client.GetItem(MakeGetInput(name)); + expect resp.Item.Some?; + + var decryptRes := encryptor.DecryptItem( + Types.DecryptItemInput( + encryptedItem:=resp.Item.value + ) + ); + + if decryptRes.Success? { + print("Accidental Success with " + name + "\n"); + } + expect decryptRes.Failure?; +} + + method {:test} TestWriteSets() { + var encryptor := GetDynamoDbItemEncryptor(); + var client :- expect DDBClientForRegion("us-west-2"); + var setParts : map := map[ + "E" := "", + "X" := "abc", + "Y" := "beaucoup de text", + "Z" := "cheers", + "1" := "1", + "2" := "2", + "2a" := "0002", + "3" := "3" + ]; + DoPutItemSS(client, encryptor, setParts, "Basic", ["X"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["X", "Y"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["Y", "X"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["X", "Y", "Z"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["X", "Z", "Y"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["Y", "X", "Z"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["Y", "Z", "X"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["Z", "X", "Y"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["Z", "Y", "X"]); + + DoPutItemNS(client, encryptor, setParts, "Num", ["1"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["1", "2"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["2", "1"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["1", "2", "3"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["1", "3", "2"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["2", "1", "3"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["2", "3", "1"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["3", "1", "2"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["3", "2", "1"]); + DoPutItemNS(client, encryptor, setParts, "Num", ["3", "2a", "1"]); + + var binParts : map> := map[ + "A" := [0, 255, 1, 254, 2, 253, 10, 13, 127, 128], + "B" := [1], + "C" := [2,3,4], + "E" := [] + ]; + DoPutItemBS(client, encryptor, binParts, "Bin", ["A"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["A", "B"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["B", "A"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["A", "B", "C"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["A", "C", "B"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["B", "A", "C"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["B", "C", "A"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["C", "A", "B"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["C", "B", "A"]); + + DoPutItemSS(client, encryptor, setParts, "Basic", ["E"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["X", "E"]); + DoPutItemSS(client, encryptor, setParts, "Basic", ["E", "X"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["E"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["C", "E"]); + DoPutItemBS(client, encryptor, binParts, "Bin", ["E", "C"]); + + var inputItem : DDB.AttributeMap := map[ + "Name" := DDB.AttributeValue.S("SetInList1"), + "TheList" := DDB.AttributeValue.L([ + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.SS(["aaa", "bbb", "ccc"]), + DDB.AttributeValue.S("zzz") + ]) + ]; + var encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + var resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInList2"), + "TheList" := DDB.AttributeValue.L([ + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + DDB.AttributeValue.S("zzz") + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInList3"), + "TheList" := DDB.AttributeValue.L([ + DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.S("zzz") + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInList4"), + "TheList" := DDB.AttributeValue.L([ + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.S("zzz"), + DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]) + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInMap1"), + "TheMap" := DDB.AttributeValue.M(map[ + "aaa" := DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + "bbb" := DDB.AttributeValue.S("zzz") + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInMapInList1"), + "TheList" := DDB.AttributeValue.L([ + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.M(map[ + "aaa" := DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + "bbb" := DDB.AttributeValue.S("zzz") + ]) + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("SetInListInMap1"), + "TheMap" := DDB.AttributeValue.M(map[ + "aaa" := DDB.AttributeValue.L([ + DDB.AttributeValue.S("aaa"), + DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + DDB.AttributeValue.S("xxx") + ]), + "bbb" := DDB.AttributeValue.S("zzz") + ]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("TwoSetsGood"), + "TheSet" := DDB.AttributeValue.SS(["aaa", "bbb", "ccc"]), + "TheSet" := DDB.AttributeValue.SS(["ddd", "eee"]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("TwoSets"), + "TheSet" := DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + "Set2" := DDB.AttributeValue.NS(["3", "1", "2", "5"]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + inputItem := map[ + "Name" := DDB.AttributeValue.S("ThreeSets"), + "TheSet" := DDB.AttributeValue.SS(["ccc", "aaa", "bbb"]), + "Set2" := DDB.AttributeValue.NS(["3", "1", "2", "5"]), + "Set3" := DDB.AttributeValue.BS([[3,2,1], [1,2,3,4]]) + ]; + encryptRes :- expect encryptor.EncryptItem(Types.EncryptItemInput(plaintextItem:=inputItem)); + resp :- expect client.PutItem(MakePutInput(encryptRes.encryptedItem)); + + GoodGet(client, encryptor, "BasicX"); + GoodGet(client, encryptor, "BasicXY"); + GoodGet(client, encryptor, "BasicXYZ"); + GoodGet(client, encryptor, "Num1"); + GoodGet(client, encryptor, "BinA"); + GoodGet(client, encryptor, "BinAB"); + GoodGet(client, encryptor, "BinABC"); + GoodGet(client, encryptor, "BasicE"); + GoodGet(client, encryptor, "BasicEX"); + GoodGet(client, encryptor, "BinE"); + GoodGet(client, encryptor, "BinEC"); + GoodGet(client, encryptor, "TwoSetsGood"); + GoodGet(client, encryptor, "SetInList1"); + + BadGet(client, encryptor, "BasicYX"); + BadGet(client, encryptor, "BasicXZY"); + BadGet(client, encryptor, "BasicZXY"); + BadGet(client, encryptor, "BasicZYX"); + BadGet(client, encryptor, "BasicYXZ"); + BadGet(client, encryptor, "BasicYZX"); + + BadGet(client, encryptor, "BinBA"); + BadGet(client, encryptor, "BinACB"); + BadGet(client, encryptor, "BinBAC"); + BadGet(client, encryptor, "BinBCA"); + BadGet(client, encryptor, "BinCAB"); + BadGet(client, encryptor, "BinCBA"); + BadGet(client, encryptor, "Num32a1"); + BadGet(client, encryptor, "SetInList2"); + BadGet(client, encryptor, "SetInList3"); + BadGet(client, encryptor, "SetInList4"); + BadGet(client, encryptor, "SetInMap1"); + BadGet(client, encryptor, "SetInListInMap1"); + BadGet(client, encryptor, "SetInMapInList1"); + BadGet(client, encryptor, "TwoSets"); + BadGet(client, encryptor, "ThreeSets"); + + } +} diff --git a/DecryptWithPermute/dafny/DecryptWithPermute/test/Validate.dfy b/DecryptWithPermute/dafny/DecryptWithPermute/test/Validate.dfy new file mode 100644 index 000000000..3a762ee96 --- /dev/null +++ b/DecryptWithPermute/dafny/DecryptWithPermute/test/Validate.dfy @@ -0,0 +1,244 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* +This test should be executed within a DB-ESDK 3.1 package, +to create example records that fail to decrypt. +*/ + +include "../src/Index.dfy" +include "../../../../DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/TestFixtures.dfy" + +module ValidateTests { + import opened Wrappers + import opened StandardLibrary.UInt + import opened DynamoDbItemEncryptorUtil + import opened AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes + import opened AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import opened AwsCryptographyDbEncryptionSdkDynamoDbTypes + import opened AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes + + import DynamoDbPermuteDecryptor + import MaterialProviders + import opened DynamoDbItemEncryptor + import AwsCryptographyMaterialProvidersTypes + import Types = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes + import DDB = ComAmazonawsDynamodbTypes + import TestFixtures + import AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations + import CSE = AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes + import SE = StructuredEncryptionUtil + import DDBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes + import AlgorithmSuites + import StandardLibrary.String + import opened Com.Amazonaws.Dynamodb + + // round trip + // encrypt => encrypted fields changed, others did not + // various errors + + function method DDBS(x : string) : DDB.AttributeValue { + DDB.AttributeValue.S(x) + } + + method GetEncryptorConfig() returns (output : DynamoDbItemEncryptorConfig) { + var keyring := TestFixtures.GetKmsKeyring(); + var logicalTableName := TestFixtures.GetTableName("SetTests"); + output := DynamoDbItemEncryptorConfig( + logicalTableName := logicalTableName, + partitionKeyName := "Name", + sortKeyName := None(), + attributeActionsOnEncrypt := map[ + "Name" := CSE.SIGN_ONLY, + "Encrypted" := CSE.ENCRYPT_AND_SIGN, + "TheSet" := CSE.SIGN_ONLY, + "Set2" := CSE.SIGN_ONLY, + "Set3" := CSE.SIGN_ONLY, + "TheMap" := CSE.SIGN_ONLY, + "TheList" := CSE.SIGN_ONLY + ], + allowedUnsignedAttributes := None(), + allowedUnsignedAttributePrefix := None(), + keyring := Some(keyring), + cmm := None(), + algorithmSuiteId := None(), + plaintextOverride := None(), + legacyOverride := None() + ); + } + + method GetDynamoDbItemEncryptorFrom(config : DynamoDbItemEncryptorConfig) + returns (encryptor: DynamoDbPermuteDecryptor.DynamoDbPermuteDecryptorClient) + ensures encryptor.ValidState() + ensures fresh(encryptor) + ensures fresh(encryptor.Modifies) + { + var keyring := TestFixtures.GetKmsKeyring(); + var encryptorConfig := DynamoDbItemEncryptorConfig( + logicalTableName := config.logicalTableName, + partitionKeyName := config.partitionKeyName, + sortKeyName := config.sortKeyName, + attributeActionsOnEncrypt := config.attributeActionsOnEncrypt, + allowedUnsignedAttributes := config.allowedUnsignedAttributes, + allowedUnsignedAttributePrefix := config.allowedUnsignedAttributePrefix, + keyring := Some(keyring), + cmm := None(), + algorithmSuiteId := None(), + legacyOverride := None(), + plaintextOverride := None() + ); + encryptor :- expect DynamoDbPermuteDecryptor.DynamoDbPermuteDecryptor(DynamoDbPermuteDecryptorConfig(inner := encryptorConfig)); + } + + method GetDynamoDbItemEncryptor() + returns (encryptor: DynamoDbPermuteDecryptor.DynamoDbPermuteDecryptorClient) + ensures encryptor.ValidState() + ensures fresh(encryptor) + ensures fresh(encryptor.Modifies) + { + var config := GetEncryptorConfig(); + encryptor := GetDynamoDbItemEncryptorFrom(config); + } + + function method MakeGetInput(name : string) : DDB.GetItemInput + { + DDB.GetItemInput( + TableName := "SetTests", + Key := map["Name" := DDB.AttributeValue.S(name)], + AttributesToGet := None, + ConsistentRead := Some(true), + ReturnConsumedCapacity := None, + ProjectionExpression := None, + ExpressionAttributeNames := None + ) + } + + datatype DecryptStatus = DecryptOk | DecryptBad + + method TestGet( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbPermuteDecryptor.DynamoDbPermuteDecryptorClient, + name : string, + stat : DecryptStatus, + permuted : bool + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies + { + print "TestGet ", name, " ", stat, " ", permuted, "\n"; + var resp :- expect client.GetItem(MakeGetInput(name)); + expect resp.Item.Some?; + + var decryptRes := encryptor.PermuteDecrypt( + PermuteDecryptInput( + inner := Types.DecryptItemInput(encryptedItem:=resp.Item.value), + maxSetSize := 7 + ) + ); + + if stat == DecryptBad { + if decryptRes.Success? { + print("Accidental Success with " + name + "\n"); + } + expect decryptRes.Failure?; + } else { + if decryptRes.Failure? { + print("Accidental Failure with " + name + "\n"); + print "\n\n",decryptRes,"\n\n"; + } + expect decryptRes.Success?; + + if permuted != decryptRes.value.didPermute { + print(name + " expected didPermute " + (if permuted then "true" else "false") + " but got the opposite.\n"); + + } + expect permuted == decryptRes.value.didPermute; + } + } + + method TestNum32a1( + client : DDB.IDynamoDBClient, + encryptor : DynamoDbPermuteDecryptor.DynamoDbPermuteDecryptorClient + ) + requires client.ValidState() + ensures client.ValidState() + modifies client.Modifies + requires encryptor.ValidState() + ensures encryptor.ValidState() + modifies encryptor.Modifies + { + print "TestNum32a1\n"; + var resp :- expect client.GetItem(MakeGetInput("Num32a1")); + expect resp.Item.Some?; + expect "TheSet" in resp.Item.value; + expect resp.Item.value["TheSet"].NS?; + expect multiset(resp.Item.value["TheSet"].NS) == multiset(["1", "2", "3"]); + var newItem := resp.Item.value["TheSet" := DDB.AttributeValue.NS(["1", "0002", "3"])]; + + var decryptRes := encryptor.PermuteDecrypt( + PermuteDecryptInput( + inner := Types.DecryptItemInput(encryptedItem:=newItem), + maxSetSize := 7 + ) + ); + + if decryptRes.Failure? { + print("Accidental Failure with TestNum32a1\n"); + print "\n\n",decryptRes,"\n\n"; + } + expect decryptRes.Success?; + + if decryptRes.value.didPermute != true { + print("TestNum32a1 expected didPermute of true but got the opposite.\n"); + } + expect decryptRes.value.didPermute == true; + } + + method {:test} TestValidate() { + var encryptor := GetDynamoDbItemEncryptor(); + var client :- expect DDBClientForRegion("us-west-2"); + + TestGet(client, encryptor, "BasicX", DecryptOk, false); + TestGet(client, encryptor, "BasicXY", DecryptOk, false); + TestGet(client, encryptor, "BasicXYZ", DecryptOk, false); + TestGet(client, encryptor, "Num1", DecryptOk, false); + TestGet(client, encryptor, "BinA", DecryptOk, false); + TestGet(client, encryptor, "BinAB", DecryptOk, false); + TestGet(client, encryptor, "BinABC", DecryptOk, false); + TestGet(client, encryptor, "BasicE", DecryptOk, false); + TestGet(client, encryptor, "BasicEX", DecryptOk, false); + TestGet(client, encryptor, "BinE", DecryptOk, false); + TestGet(client, encryptor, "BinEC", DecryptOk, false); + TestGet(client, encryptor, "TwoSetsGood", DecryptOk, false); + TestGet(client, encryptor, "SetInList1", DecryptOk, false); + + TestGet(client, encryptor, "BasicYX", DecryptOk, true); + TestGet(client, encryptor, "BasicXZY", DecryptOk, true); + TestGet(client, encryptor, "BasicZXY", DecryptOk, true); + TestGet(client, encryptor, "BasicZYX", DecryptOk, true); + TestGet(client, encryptor, "BasicYXZ", DecryptOk, true); + TestGet(client, encryptor, "BasicYZX", DecryptOk, true); + + TestGet(client, encryptor, "BinBA", DecryptOk, true); + TestGet(client, encryptor, "BinACB", DecryptOk, true); + TestGet(client, encryptor, "BinBAC", DecryptOk, true); + TestGet(client, encryptor, "BinBCA", DecryptOk, true); + TestGet(client, encryptor, "BinCAB", DecryptOk, true); + TestGet(client, encryptor, "BinCBA", DecryptOk, true); + TestGet(client, encryptor, "SetInList2", DecryptOk, true); + TestGet(client, encryptor, "SetInList3", DecryptOk, true); + TestGet(client, encryptor, "SetInList4", DecryptOk, true); + TestGet(client, encryptor, "SetInMap1", DecryptOk, true); + TestGet(client, encryptor, "SetInListInMap1", DecryptOk, true); + TestGet(client, encryptor, "SetInMapInList1", DecryptOk, true); + TestGet(client, encryptor, "TwoSets", DecryptOk, true); + TestGet(client, encryptor, "ThreeSets", DecryptOk, true); + + TestGet(client, encryptor, "Num32a1", DecryptBad, true); + TestNum32a1(client, encryptor); + } +} diff --git a/DecryptWithPermute/runtimes/java/build.gradle.kts b/DecryptWithPermute/runtimes/java/build.gradle.kts new file mode 100644 index 000000000..d5e464531 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/build.gradle.kts @@ -0,0 +1,268 @@ +import java.net.URI +import javax.annotation.Nullable +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + `java` + `java-library` + `maven-publish` + `signing` + id("io.github.gradle-nexus.publish-plugin") version "1.3.0" +} + +group = "software.amazon.cryptography" +version = "3.1.1" +description = "Aws Database Encryption Sdk for DynamoDb Java" + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(8)) + sourceSets["main"].java { + srcDir("src/main/java") + srcDir("src/main/dafny-generated") + srcDir("src/main/smithy-generated") + srcDir("src/main/sdkv1") + } + sourceSets["test"].java { + srcDir("src/test") + } + withJavadocJar() + withSourcesJar() +} + +var caUrl: URI? = null +@Nullable +val caUrlStr: String? = System.getenv("CODEARTIFACT_URL_JAVA_CONVERSION") +if (!caUrlStr.isNullOrBlank()) { + caUrl = URI.create(caUrlStr) +} + +var caPassword: String? = null +@Nullable +val caPasswordString: String? = System.getenv("CODEARTIFACT_AUTH_TOKEN") +if (!caPasswordString.isNullOrBlank()) { + caPassword = caPasswordString +} + +repositories { + maven { + name = "DynamoDB Local Release Repository - US West (Oregon) Region" + url = URI.create("https://s3-us-west-2.amazonaws.com/dynamodb-local/release") + } + mavenCentral() + mavenLocal() + if (caUrl != null && caPassword != null) { + maven { + name = "CodeArtifact" + url = caUrl!! + credentials { + username = "aws" + password = caPassword!! + } + } + } +} + +// Configuration to hold SQLLite information. +// DynamoDB-Local needs to have access to native sqllite4java. +val dynamodb by configurations.creating + +dependencies { + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") + implementation("org.dafny:DafnyRuntime:4.1.0") + implementation("software.amazon.smithy.dafny:conversion:0.1") + implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") + + implementation(platform("software.amazon.awssdk:bom:2.20.128")) + implementation("software.amazon.awssdk:dynamodb") + implementation("software.amazon.awssdk:dynamodb-enhanced") + implementation("software.amazon.awssdk:kms") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + + // For the DDB-EC v1 + implementation("com.amazonaws:aws-java-sdk-dynamodb:1.12.531") + // https://mvnrepository.com/artifact/org.testng/testng + testImplementation("org.testng:testng:7.5") + // https://mvnrepository.com/artifact/com.amazonaws/DynamoDBLocal + testImplementation("com.amazonaws:DynamoDBLocal:1.+") + // This is where we gather the SQLLite files to copy over + dynamodb("com.amazonaws:DynamoDBLocal:1.+") + // As of 1.21.0 DynamoDBLocal does not support Apple Silicon + // This checks the dependencies and adds a native library + // to support this architecture. + if (org.apache.tools.ant.taskdefs.condition.Os.isArch("aarch64")) { + testImplementation("io.github.ganadist.sqlite4java:libsqlite4java-osx-aarch64:1.0.392") + dynamodb("io.github.ganadist.sqlite4java:libsqlite4java-osx-aarch64:1.0.392") + } + // https://mvnrepository.com/artifact/org.hamcrest/hamcrest-all + testImplementation("org.hamcrest:hamcrest-all:1.3") + // https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on + testImplementation("org.bouncycastle:bcprov-jdk15on:1.70") + // https://mvnrepository.com/artifact/org.quicktheories/quicktheories + testImplementation("org.quicktheories:quicktheories:0.26") + // https://mvnrepository.com/artifact/junit/junit + testImplementation("junit:junit:4.13.2") + // https://mvnrepository.com/artifact/edu.umd.cs.mtc/multithreadedtc + testImplementation("edu.umd.cs.mtc:multithreadedtc:1.01") + // https://mvnrepository.com/artifact/org.projectlombok/lombok + testImplementation("org.projectlombok:lombok:1.18.28") + testAnnotationProcessor("org.projectlombok:lombok:1.18.28") +} + +publishing { + publications.create("mavenLocal") { + groupId = "software.amazon.cryptography" + artifactId = "aws-database-encryption-sdk-dynamodb" + from(components["java"]) + } + + publications.create("maven") { + groupId = "software.amazon.cryptography" + artifactId = "aws-database-encryption-sdk-dynamodb" + from(components["java"]) + + // Include extra information in the POMs. + afterEvaluate { + pom { + name.set("AWS Database Encryption SDK for DynamoDB") + description.set("AWS Database Encryption SDK for DynamoDB in Java") + url.set("https://github.com/aws/aws-database-encryption-sdk-dynamodb-java") + licenses { + license { + name.set("Apache License 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + developers { + developer { + id.set("amazonwebservices") + organization.set("Amazon Web Services") + organizationUrl.set("https://aws.amazon.com") + roles.add("developer") + } + } + scm { + url.set("https://github.com/aws/aws-database-encryption-sdk-dynamodb-java.git") + } + } + } + } + repositories { + mavenLocal() + maven { + name = "StagingCodeArtifact" + url = URI.create("https://crypto-tools-internal-587316601012.d.codeartifact.us-east-1.amazonaws.com/maven/java-dbesdk-ddb-staging/") + credentials { + username = "aws" + password = System.getenv("CODEARTIFACT_TOKEN") + } + } + } +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + // to compile a sources jar we need a strategy on how to deal with duplicates; + // we choose to include duplicate classes. + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.test { + useTestNG() + dependsOn("CopyDynamoDb") + systemProperty("java.library.path", "build/libs") + + // This will show System.out.println statements + testLogging.showStandardStreams = true + + testLogging { + lifecycle { + events = mutableSetOf(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED) + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + showStandardStreams = true + } + info.events = lifecycle.events + info.exceptionFormat = lifecycle.exceptionFormat + } + + // See https://github.com/gradle/kotlin-dsl/issues/836 + addTestListener(object : TestListener { + override fun beforeSuite(suite: TestDescriptor) {} + override fun beforeTest(testDescriptor: TestDescriptor) {} + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {} + + override fun afterSuite(suite: TestDescriptor, result: TestResult) { + if (suite.parent == null) { // root suite + logger.lifecycle("----") + logger.lifecycle("Test result: ${result.resultType}") + logger.lifecycle("Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped") + } + } + }) +} + +tasks.register("runTests") { + mainClass.set("TestsFromDafny") + classpath = sourceSets["test"].runtimeClasspath +} + +tasks.register("CopyDynamoDb") { + from (dynamodb) { + include("*.dll") + include("*.dylib") + include("*.so") + } + into("build/libs") +} + +tasks.javadoc { + options { + (this as CoreJavadocOptions).addStringOption("Xdoclint:none", "-quiet") + } + exclude("src/main/dafny-generated") +} + +nexusPublishing { + // We are using the nexusPublishing plugin since it is recommended by Sonatype Gradle Project configurations + // and it is easy to supply the creds we need to deploy + // https://github.com/gradle-nexus/publish-plugin/ + repositories { + sonatype { + nexusUrl.set(uri("https://aws.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://aws.oss.sonatype.org/content/repositories/snapshots/")) + username.set(System.getenv("SONA_USERNAME")) + password.set(System.getenv("SONA_PASSWORD")) + } + } +} + +signing { + useGpgCmd() + + // Dynamically set these properties + project.ext.set("signing.gnupg.executable", "gpg") + project.ext.set("signing.gnupg.useLegacyGpg" , "true") + project.ext.set("signing.gnupg.homeDir", System.getenv("HOME") + "/.gnupg/") + project.ext.set("signing.gnupg.optionsFile", System.getenv("HOME") + "/.gnupg/gpg.conf") + project.ext.set("signing.gnupg.keyName", System.getenv("GPG_KEY")) + project.ext.set("signing.gnupg.passphrase", System.getenv("GPG_PASS")) + + // Signing is required if building a release version and if we're going to publish it. + // Otherwise if doing a maven publication we will sign + setRequired({ + gradle.getTaskGraph().hasTask("publish") + }) + sign(publishing.publications["maven"]) +} diff --git a/DecryptWithPermute/runtimes/java/gradle.properties b/DecryptWithPermute/runtimes/java/gradle.properties new file mode 100644 index 000000000..0fd7c658d --- /dev/null +++ b/DecryptWithPermute/runtimes/java/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx4096M \ No newline at end of file diff --git a/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.jar b/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..943f0cbfa Binary files /dev/null and b/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.properties b/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..f398c33c4 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/DecryptWithPermute/runtimes/java/gradlew b/DecryptWithPermute/runtimes/java/gradlew new file mode 100755 index 000000000..65dcd68d6 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/DecryptWithPermute/runtimes/java/gradlew.bat b/DecryptWithPermute/runtimes/java/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /dev/null +++ b/DecryptWithPermute/runtimes/java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/DecryptWithPermute/runtimes/java/makefile_helper.sh b/DecryptWithPermute/runtimes/java/makefile_helper.sh new file mode 100755 index 000000000..863490e61 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/makefile_helper.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [[ "$1" == "implementation" ]]; then + mkdir -p runtimes/java/src/main/dafny-generated + for directory in runtimes/java/temp/AwsCryptographyDbEncryptionSdkDynamoDbMiddlewareInternal-java/*; do + mv "$directory" "runtimes/java/src/main/dafny-generated/" + done + exit 0 +elif [[ "$1" == "test" ]]; then + # With the runAllTests flag, dafny generates an entry point for the tests that we want, + # so we copy all files to the test destination. + mkdir -p runtimes/java/src/test/dafny-generated + for allFiles in runtimes/java/temp-tests/AwsCryptographyDbEncryptionSdkDynamoDbMiddlewareInternalTests-java/*; do + mv "$allFiles" "runtimes/java/src/test/dafny-generated/" + done + exit 0 +else + echo "makefile_helper needs either implementation or test argument" + echo "i.e: ./runtimes/java/makefile_helper.sh test" + exit 1 +fi diff --git a/DecryptWithPermute/runtimes/java/settings.gradle.kts b/DecryptWithPermute/runtimes/java/settings.gradle.kts new file mode 100644 index 000000000..0adbe0dd3 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "AwsCryptographyDbEncryptionSdkDynamoDb" diff --git a/DecryptWithPermute/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/internaldafny/__default.java b/DecryptWithPermute/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/internaldafny/__default.java new file mode 100644 index 000000000..549bd9926 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/internaldafny/__default.java @@ -0,0 +1,4 @@ +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny; + +public class __default extends software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny._ExternBase___default { +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/DynamoDbPermuteDecryptor.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/DynamoDbPermuteDecryptor.java new file mode 100644 index 000000000..e3f51d8ed --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/DynamoDbPermuteDecryptor.java @@ -0,0 +1,93 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute; + +import Wrappers_Compile.Result; +import java.lang.IllegalArgumentException; +import java.util.Objects; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.DynamoDbPermuteDecryptorClient; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.__default; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.IDynamoDbPermuteDecryptorClient; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.DynamoDbPermuteDecryptorConfig; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptInput; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptOutput; + +public class DynamoDbPermuteDecryptor { + private final IDynamoDbPermuteDecryptorClient _impl; + + protected DynamoDbPermuteDecryptor(BuilderImpl builder) { + DynamoDbPermuteDecryptorConfig input = builder.DynamoDbPermuteDecryptorConfig(); + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.DynamoDbPermuteDecryptorConfig dafnyValue = ToDafny.DynamoDbPermuteDecryptorConfig(input); + Result result = __default.DynamoDbPermuteDecryptor(dafnyValue); + if (result.is_Failure()) { + throw ToNative.Error(result.dtor_error()); + } + this._impl = result.dtor_value(); + } + + DynamoDbPermuteDecryptor(IDynamoDbPermuteDecryptorClient impl) { + this._impl = impl; + } + + public static Builder builder() { + return new BuilderImpl(); + } + + /** + * Decrypt a DynamoDB Item, permute sets as necessary to match signature. + * @return Outputs for decrypting a DynamoDB Item. + */ + public PermuteDecryptOutput PermuteDecrypt(PermuteDecryptInput input) { + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.PermuteDecryptInput dafnyValue = ToDafny.PermuteDecryptInput(input); + Result result = this._impl.PermuteDecrypt(dafnyValue); + if (result.is_Failure()) { + throw ToNative.Error(result.dtor_error()); + } + return ToNative.PermuteDecryptOutput(result.dtor_value()); + } + + protected IDynamoDbPermuteDecryptorClient impl() { + return this._impl; + } + + public interface Builder { + /** + * @param DynamoDbPermuteDecryptorConfig The configuration for the client-side encryption of DynamoDB items. + */ + Builder DynamoDbPermuteDecryptorConfig( + DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig); + + /** + * @return The configuration for the client-side encryption of DynamoDB items. + */ + DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig(); + + DynamoDbPermuteDecryptor build(); + } + + static class BuilderImpl implements Builder { + protected DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig; + + protected BuilderImpl() { + } + + public Builder DynamoDbPermuteDecryptorConfig( + DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig) { + this.DynamoDbPermuteDecryptorConfig = DynamoDbPermuteDecryptorConfig; + return this; + } + + public DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig() { + return this.DynamoDbPermuteDecryptorConfig; + } + + public DynamoDbPermuteDecryptor build() { + if (Objects.isNull(this.DynamoDbPermuteDecryptorConfig())) { + throw new IllegalArgumentException("Missing value for required field `DynamoDbPermuteDecryptorConfig`"); + } + return new DynamoDbPermuteDecryptor(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToDafny.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToDafny.java new file mode 100644 index 000000000..4eeefbcca --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToDafny.java @@ -0,0 +1,86 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute; + +import dafny.DafnySequence; +import java.lang.Boolean; +import java.lang.Character; +import java.lang.Integer; +import java.lang.RuntimeException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.DynamoDbPermuteDecryptorConfig; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error_DynamoDbPermuteDecryptorException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.IDynamoDbPermuteDecryptorClient; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.PermuteDecryptInput; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.PermuteDecryptOutput; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.CollectionOfErrors; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.DynamoDbPermuteDecryptorException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.OpaqueError; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.internaldafny.types.DecryptItemInput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.internaldafny.types.DecryptItemOutput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.internaldafny.types.DynamoDbItemEncryptorConfig; + +public class ToDafny { + public static Error Error(RuntimeException nativeValue) { + if (nativeValue instanceof DynamoDbPermuteDecryptorException) { + return ToDafny.Error((DynamoDbPermuteDecryptorException) nativeValue); + } + if (nativeValue instanceof OpaqueError) { + return ToDafny.Error((OpaqueError) nativeValue); + } + if (nativeValue instanceof CollectionOfErrors) { + return ToDafny.Error((CollectionOfErrors) nativeValue); + } + return Error.create_Opaque(nativeValue); + } + + public static Error Error(OpaqueError nativeValue) { + return Error.create_Opaque(nativeValue.obj()); + } + + public static Error Error(CollectionOfErrors nativeValue) { + DafnySequence list = software.amazon.smithy.dafny.conversion.ToDafny.Aggregate.GenericToSequence( + nativeValue.list(), + ToDafny::Error, + Error._typeDescriptor()); + DafnySequence message = software.amazon.smithy.dafny.conversion.ToDafny.Simple.CharacterSequence(nativeValue.getMessage()); + return Error.create_CollectionOfErrors(list, message); + } + + public static DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.DynamoDbPermuteDecryptorConfig nativeValue) { + DynamoDbItemEncryptorConfig inner; + inner = software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToDafny.DynamoDbItemEncryptorConfig(nativeValue.inner()); + return new DynamoDbPermuteDecryptorConfig(inner); + } + + public static PermuteDecryptInput PermuteDecryptInput( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptInput nativeValue) { + DecryptItemInput inner; + inner = software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToDafny.DecryptItemInput(nativeValue.inner()); + Integer maxSetSize; + maxSetSize = (nativeValue.maxSetSize()); + return new PermuteDecryptInput(inner, maxSetSize); + } + + public static PermuteDecryptOutput PermuteDecryptOutput( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptOutput nativeValue) { + DecryptItemOutput inner; + inner = software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToDafny.DecryptItemOutput(nativeValue.inner()); + Boolean didPermute; + didPermute = (nativeValue.didPermute()); + return new PermuteDecryptOutput(inner, didPermute); + } + + public static Error Error(DynamoDbPermuteDecryptorException nativeValue) { + DafnySequence message; + message = software.amazon.smithy.dafny.conversion.ToDafny.Simple.CharacterSequence(nativeValue.message()); + return new Error_DynamoDbPermuteDecryptorException(message); + } + + public static IDynamoDbPermuteDecryptorClient DynamoDbPermuteDecryptor( + DynamoDbPermuteDecryptor nativeValue) { + return nativeValue.impl(); + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToNative.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToNative.java new file mode 100644 index 000000000..fbded8b11 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToNative.java @@ -0,0 +1,97 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute; + +import java.lang.RuntimeException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error_CollectionOfErrors; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error_DynamoDbPermuteDecryptorException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.Error_Opaque; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.IDynamoDbPermuteDecryptorClient; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.CollectionOfErrors; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.DynamoDbPermuteDecryptorConfig; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.DynamoDbPermuteDecryptorException; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.OpaqueError; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptInput; +import software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model.PermuteDecryptOutput; + +public class ToNative { + public static OpaqueError Error(Error_Opaque dafnyValue) { + OpaqueError.Builder nativeBuilder = OpaqueError.builder(); + nativeBuilder.obj(dafnyValue.dtor_obj()); + return nativeBuilder.build(); + } + + public static CollectionOfErrors Error(Error_CollectionOfErrors dafnyValue) { + CollectionOfErrors.Builder nativeBuilder = CollectionOfErrors.builder(); + nativeBuilder.list( + software.amazon.smithy.dafny.conversion.ToNative.Aggregate.GenericToList( + dafnyValue.dtor_list(), + ToNative::Error)); + nativeBuilder.message(software.amazon.smithy.dafny.conversion.ToNative.Simple.String(dafnyValue.dtor_message())); + return nativeBuilder.build(); + } + + public static DynamoDbPermuteDecryptorException Error( + Error_DynamoDbPermuteDecryptorException dafnyValue) { + DynamoDbPermuteDecryptorException.Builder nativeBuilder = DynamoDbPermuteDecryptorException.builder(); + nativeBuilder.message(software.amazon.smithy.dafny.conversion.ToNative.Simple.String(dafnyValue.dtor_message())); + return nativeBuilder.build(); + } + + public static RuntimeException Error(Error dafnyValue) { + if (dafnyValue.is_DynamoDbPermuteDecryptorException()) { + return ToNative.Error((Error_DynamoDbPermuteDecryptorException) dafnyValue); + } + if (dafnyValue.is_Opaque()) { + return ToNative.Error((Error_Opaque) dafnyValue); + } + if (dafnyValue.is_CollectionOfErrors()) { + return ToNative.Error((Error_CollectionOfErrors) dafnyValue); + } + if (dafnyValue.is_AwsCryptographyMaterialProviders()) { + return software.amazon.cryptography.materialproviders.ToNative.Error(dafnyValue.dtor_AwsCryptographyMaterialProviders()); + } + if (dafnyValue.is_AwsCryptographyDbEncryptionSdkStructuredEncryption()) { + return software.amazon.cryptography.dbencryptionsdk.structuredencryption.ToNative.Error(dafnyValue.dtor_AwsCryptographyDbEncryptionSdkStructuredEncryption()); + } + if (dafnyValue.is_AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor()) { + return software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToNative.Error(dafnyValue.dtor_AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor()); + } + if (dafnyValue.is_AwsCryptographyDbEncryptionSdkDynamoDb()) { + return software.amazon.cryptography.dbencryptionsdk.dynamodb.ToNative.Error(dafnyValue.dtor_AwsCryptographyDbEncryptionSdkDynamoDb()); + } + OpaqueError.Builder nativeBuilder = OpaqueError.builder(); + nativeBuilder.obj(dafnyValue); + return nativeBuilder.build(); + } + + public static DynamoDbPermuteDecryptorConfig DynamoDbPermuteDecryptorConfig( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.DynamoDbPermuteDecryptorConfig dafnyValue) { + DynamoDbPermuteDecryptorConfig.Builder nativeBuilder = DynamoDbPermuteDecryptorConfig.builder(); + nativeBuilder.inner(software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToNative.DynamoDbItemEncryptorConfig(dafnyValue.dtor_inner())); + return nativeBuilder.build(); + } + + public static PermuteDecryptInput PermuteDecryptInput( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.PermuteDecryptInput dafnyValue) { + PermuteDecryptInput.Builder nativeBuilder = PermuteDecryptInput.builder(); + nativeBuilder.inner(software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToNative.DecryptItemInput(dafnyValue.dtor_inner())); + nativeBuilder.maxSetSize((dafnyValue.dtor_maxSetSize())); + return nativeBuilder.build(); + } + + public static PermuteDecryptOutput PermuteDecryptOutput( + software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.internaldafny.types.PermuteDecryptOutput dafnyValue) { + PermuteDecryptOutput.Builder nativeBuilder = PermuteDecryptOutput.builder(); + nativeBuilder.inner(software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.ToNative.DecryptItemOutput(dafnyValue.dtor_inner())); + nativeBuilder.didPermute((dafnyValue.dtor_didPermute())); + return nativeBuilder.build(); + } + + public static DynamoDbPermuteDecryptor DynamoDbPermuteDecryptor( + IDynamoDbPermuteDecryptorClient dafnyValue) { + return new DynamoDbPermuteDecryptor(dafnyValue); + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/CollectionOfErrors.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/CollectionOfErrors.java new file mode 100644 index 000000000..7af2b5158 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/CollectionOfErrors.java @@ -0,0 +1,139 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +import java.util.List; + +public class CollectionOfErrors extends RuntimeException { + /** + * The list of Exceptions encountered. + */ + private final List list; + + protected CollectionOfErrors(BuilderImpl builder) { + super(messageFromBuilder(builder), builder.cause()); + this.list = builder.list(); + } + + private static String messageFromBuilder(Builder builder) { + if (builder.message() != null) { + return builder.message(); + } + if (builder.cause() != null) { + return builder.cause().getMessage(); + } + return null; + } + + /** + * See {@link Throwable#getMessage()}. + */ + public String message() { + return this.getMessage(); + } + + /** + * See {@link Throwable#getCause()}. + */ + public Throwable cause() { + return this.getCause(); + } + + /** + * @return The list of Exceptions encountered. + */ + public List list() { + return this.list; + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param message The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + Builder message(String message); + + /** + * @return The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + String message(); + + /** + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Builder cause(Throwable cause); + + /** + * @return The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Throwable cause(); + + /** + * @param list The list of Exceptions encountered. + */ + Builder list(List list); + + /** + * @return The list of Exceptions encountered. + */ + List list(); + + CollectionOfErrors build(); + } + + static class BuilderImpl implements Builder { + protected String message; + + protected Throwable cause; + + protected List list; + + protected BuilderImpl() { + } + + protected BuilderImpl(CollectionOfErrors model) { + this.cause = model.getCause(); + this.message = model.getMessage(); + this.list = model.list(); + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public String message() { + return this.message; + } + + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + public Throwable cause() { + return this.cause; + } + + public Builder list(List list) { + this.list = list; + return this; + } + + public List list() { + return this.list; + } + + public CollectionOfErrors build() { + return new CollectionOfErrors(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorConfig.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorConfig.java new file mode 100644 index 000000000..e62bfe67e --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorConfig.java @@ -0,0 +1,77 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +import java.util.Objects; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DynamoDbItemEncryptorConfig; + +/** + * The configuration for the client-side encryption of DynamoDB items. + */ +public class DynamoDbPermuteDecryptorConfig { + /** + * The configuration for the client-side encryption of DynamoDB items. + */ + private final DynamoDbItemEncryptorConfig inner; + + protected DynamoDbPermuteDecryptorConfig(BuilderImpl builder) { + this.inner = builder.inner(); + } + + /** + * @return The configuration for the client-side encryption of DynamoDB items. + */ + public DynamoDbItemEncryptorConfig inner() { + return this.inner; + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param inner The configuration for the client-side encryption of DynamoDB items. + */ + Builder inner(DynamoDbItemEncryptorConfig inner); + + /** + * @return The configuration for the client-side encryption of DynamoDB items. + */ + DynamoDbItemEncryptorConfig inner(); + + DynamoDbPermuteDecryptorConfig build(); + } + + static class BuilderImpl implements Builder { + protected DynamoDbItemEncryptorConfig inner; + + protected BuilderImpl() { + } + + protected BuilderImpl(DynamoDbPermuteDecryptorConfig model) { + this.inner = model.inner(); + } + + public Builder inner(DynamoDbItemEncryptorConfig inner) { + this.inner = inner; + return this; + } + + public DynamoDbItemEncryptorConfig inner() { + return this.inner; + } + + public DynamoDbPermuteDecryptorConfig build() { + if (Objects.isNull(this.inner())) { + throw new IllegalArgumentException("Missing value for required field `inner`"); + } + return new DynamoDbPermuteDecryptorConfig(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorException.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorException.java new file mode 100644 index 000000000..c80ce6d4f --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorException.java @@ -0,0 +1,107 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +import java.util.Objects; + +public class DynamoDbPermuteDecryptorException extends RuntimeException { + protected DynamoDbPermuteDecryptorException(BuilderImpl builder) { + super(messageFromBuilder(builder), builder.cause()); + } + + private static String messageFromBuilder(Builder builder) { + if (builder.message() != null) { + return builder.message(); + } + if (builder.cause() != null) { + return builder.cause().getMessage(); + } + return null; + } + + /** + * See {@link Throwable#getMessage()}. + */ + public String message() { + return this.getMessage(); + } + + /** + * See {@link Throwable#getCause()}. + */ + public Throwable cause() { + return this.getCause(); + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param message The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + Builder message(String message); + + /** + * @return The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + String message(); + + /** + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Builder cause(Throwable cause); + + /** + * @return The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Throwable cause(); + + DynamoDbPermuteDecryptorException build(); + } + + static class BuilderImpl implements Builder { + protected String message; + + protected Throwable cause; + + protected BuilderImpl() { + } + + protected BuilderImpl(DynamoDbPermuteDecryptorException model) { + this.message = model.message(); + this.cause = model.cause(); + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public String message() { + return this.message; + } + + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + public Throwable cause() { + return this.cause; + } + + public DynamoDbPermuteDecryptorException build() { + if (Objects.isNull(this.message())) { + throw new IllegalArgumentException("Missing value for required field `message`"); + } + return new DynamoDbPermuteDecryptorException(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/OpaqueError.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/OpaqueError.java new file mode 100644 index 000000000..5ec24938e --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/OpaqueError.java @@ -0,0 +1,142 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +public class OpaqueError extends RuntimeException { + /** + * The unexpected object encountered. It MIGHT BE an Exception, but that is not guaranteed. + */ + private final Object obj; + + protected OpaqueError(BuilderImpl builder) { + super(messageFromBuilder(builder), builder.cause()); + this.obj = builder.obj(); + } + + private static String messageFromBuilder(Builder builder) { + if (builder.message() != null) { + return builder.message(); + } + if (builder.cause() != null) { + return builder.cause().getMessage(); + } + return null; + } + + /** + * See {@link Throwable#getMessage()}. + */ + public String message() { + return this.getMessage(); + } + + /** + * See {@link Throwable#getCause()}. + */ + public Throwable cause() { + return this.getCause(); + } + + /** + * @return The unexpected object encountered. It MIGHT BE an Exception, but that is not guaranteed. + */ + public Object obj() { + return this.obj; + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param message The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + Builder message(String message); + + /** + * @return The detailed message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + String message(); + + /** + * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Builder cause(Throwable cause); + + /** + * @return The cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) + */ + Throwable cause(); + + /** + * @param obj The unexpected object encountered. It MIGHT BE an Exception, but that is not guaranteed. + */ + Builder obj(Object obj); + + /** + * @return The unexpected object encountered. It MIGHT BE an Exception, but that is not guaranteed. + */ + Object obj(); + + OpaqueError build(); + } + + static class BuilderImpl implements Builder { + protected String message; + + protected Throwable cause; + + protected Object obj; + + protected BuilderImpl() { + } + + protected BuilderImpl(OpaqueError model) { + this.cause = model.getCause(); + this.message = model.getMessage(); + this.obj = model.obj(); + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public String message() { + return this.message; + } + + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + public Throwable cause() { + return this.cause; + } + + public Builder obj(Object obj) { + this.obj = obj; + return this; + } + + public Object obj() { + return this.obj; + } + + public OpaqueError build() { + if (this.obj != null && this.cause == null && this.obj instanceof Throwable) { + this.cause = (Throwable) this.obj; + } else if (this.obj == null && this.cause != null) { + this.obj = this.cause; + } + return new OpaqueError(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptInput.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptInput.java new file mode 100644 index 000000000..253edb681 --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptInput.java @@ -0,0 +1,103 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +import java.util.Objects; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DecryptItemInput; + +public class PermuteDecryptInput { + /** + * Inputs for decrypting a DynamoDB Item. + */ + private final DecryptItemInput inner; + + private final Integer maxSetSize; + + protected PermuteDecryptInput(BuilderImpl builder) { + this.inner = builder.inner(); + this.maxSetSize = builder.maxSetSize(); + } + + /** + * @return Inputs for decrypting a DynamoDB Item. + */ + public DecryptItemInput inner() { + return this.inner; + } + + public Integer maxSetSize() { + return this.maxSetSize; + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param inner Inputs for decrypting a DynamoDB Item. + */ + Builder inner(DecryptItemInput inner); + + /** + * @return Inputs for decrypting a DynamoDB Item. + */ + DecryptItemInput inner(); + + Builder maxSetSize(Integer maxSetSize); + + Integer maxSetSize(); + + PermuteDecryptInput build(); + } + + static class BuilderImpl implements Builder { + protected DecryptItemInput inner; + + protected Integer maxSetSize; + + protected BuilderImpl() { + } + + protected BuilderImpl(PermuteDecryptInput model) { + this.inner = model.inner(); + this.maxSetSize = model.maxSetSize(); + } + + public Builder inner(DecryptItemInput inner) { + this.inner = inner; + return this; + } + + public DecryptItemInput inner() { + return this.inner; + } + + public Builder maxSetSize(Integer maxSetSize) { + this.maxSetSize = maxSetSize; + return this; + } + + public Integer maxSetSize() { + return this.maxSetSize; + } + + public PermuteDecryptInput build() { + if (Objects.isNull(this.inner())) { + throw new IllegalArgumentException("Missing value for required field `inner`"); + } + if (Objects.isNull(this.maxSetSize())) { + throw new IllegalArgumentException("Missing value for required field `maxSetSize`"); + } + if (Objects.nonNull(this.maxSetSize()) && this.maxSetSize() < 0) { + throw new IllegalArgumentException("`maxSetSize` must be greater than or equal to 0"); + } + return new PermuteDecryptInput(this); + } + } +} diff --git a/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptOutput.java b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptOutput.java new file mode 100644 index 000000000..2710b4eba --- /dev/null +++ b/DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptOutput.java @@ -0,0 +1,103 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Do not modify this file. This file is machine generated, and any changes to it will be overwritten. +package software.amazon.cryptography.dbencryptionsdk.decryptwithpermute.model; + +import java.util.Objects; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DecryptItemOutput; + +/** + * Outputs for decrypting a DynamoDB Item. + */ +public class PermuteDecryptOutput { + /** + * Outputs for decrypting a DynamoDB Item. + */ + private final DecryptItemOutput inner; + + private final Boolean didPermute; + + protected PermuteDecryptOutput(BuilderImpl builder) { + this.inner = builder.inner(); + this.didPermute = builder.didPermute(); + } + + /** + * @return Outputs for decrypting a DynamoDB Item. + */ + public DecryptItemOutput inner() { + return this.inner; + } + + public Boolean didPermute() { + return this.didPermute; + } + + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + /** + * @param inner Outputs for decrypting a DynamoDB Item. + */ + Builder inner(DecryptItemOutput inner); + + /** + * @return Outputs for decrypting a DynamoDB Item. + */ + DecryptItemOutput inner(); + + Builder didPermute(Boolean didPermute); + + Boolean didPermute(); + + PermuteDecryptOutput build(); + } + + static class BuilderImpl implements Builder { + protected DecryptItemOutput inner; + + protected Boolean didPermute; + + protected BuilderImpl() { + } + + protected BuilderImpl(PermuteDecryptOutput model) { + this.inner = model.inner(); + this.didPermute = model.didPermute(); + } + + public Builder inner(DecryptItemOutput inner) { + this.inner = inner; + return this; + } + + public DecryptItemOutput inner() { + return this.inner; + } + + public Builder didPermute(Boolean didPermute) { + this.didPermute = didPermute; + return this; + } + + public Boolean didPermute() { + return this.didPermute; + } + + public PermuteDecryptOutput build() { + if (Objects.isNull(this.inner())) { + throw new IllegalArgumentException("Missing value for required field `inner`"); + } + if (Objects.isNull(this.didPermute())) { + throw new IllegalArgumentException("Missing value for required field `didPermute`"); + } + return new PermuteDecryptOutput(this); + } + } +} diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/ConfigToInfo.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/ConfigToInfo.dfy index c4100927a..3efbfdeb1 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/ConfigToInfo.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/ConfigToInfo.dfy @@ -310,6 +310,7 @@ module SearchConfigToInfo { if |badNames| == 0 then None else + // We happen to order these values, but this ordering MUST NOT be relied upon. var badSeq := SortedSets.ComputeSetToOrderedSequence2(badNames, CharLess); Some(badSeq[0]) } @@ -381,6 +382,7 @@ module SearchConfigToInfo { if |badNames| == 0 then None else + // We happen to order these values, but this ordering MUST NOT be relied upon. var badSeq := SortedSets.ComputeSetToOrderedSequence2(badNames, CharLess); Some(badSeq[0]) } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DDBSupport.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DDBSupport.dfy index 05088cada..1f055f8c9 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DDBSupport.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DDBSupport.dfy @@ -44,6 +44,7 @@ module DynamoDBSupport { Success(true) else var bad := set k <- item | ReservedPrefix <= k; + // We happen to order these values, but this ordering MUST NOT be relied upon. var badSeq := SortedSets.ComputeSetToOrderedSequence2(bad, CharLess); if |badSeq| == 0 then Failure("") @@ -173,6 +174,7 @@ module DynamoDBSupport { var both := newAttrs.Keys * item.Keys; var bad := set k <- both | newAttrs[k] != item[k]; if 0 < |bad| { + // We happen to order these values, but this ordering MUST NOT be relied upon. var badSeq := SortedSets.ComputeSetToOrderedSequence2(bad, CharLess); return Failure(E("Supplied Beacons do not match calculated beacons : " + Join(badSeq, ", "))); } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DynamoToStruct.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DynamoToStruct.dfy index 8d281ed69..e10ab8f44 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DynamoToStruct.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/DynamoToStruct.dfy @@ -23,15 +23,8 @@ module DynamoToStruct { type StructuredDataTerminalType = x : StructuredData | x.content.Terminal? witness * type TerminalDataMap = map -//= specification/dynamodb-encryption-client/ddb-item-conversion.md#overview -//= type=TODO -//# The conversion from DDB Item to Structured Data must be lossless, -//# meaning that converting a DDB Item to -//# a Structured Data and back to a DDB Item again -//# MUST result in the exact same DDB Item. - // This file exists for these two functions : ItemToStructured and StructuredToItem - // which provide lossless conversion between an AttributeMap and a StructuredDataMap + // which provide conversion between an AttributeMap and a StructuredDataMap // Convert AttributeMap to StructuredDataMap //= specification/dynamodb-encryption-client/ddb-item-conversion.md#convert-ddb-item-to-structured-data @@ -107,6 +100,7 @@ module DynamoToStruct { else var badNames := set k <- s | !IsValid_AttributeName(k) :: k; OneBadKey(s, badNames, IsValid_AttributeName); + // We happen to order these values, but this ordering MUST NOT be relied upon. var orderedAttrNames := SetToOrderedSequence(badNames, CharLess); var attrNameList := Join(orderedAttrNames, ","); MakeError("Not valid attribute names : " + attrNameList) @@ -317,7 +311,11 @@ module DynamoToStruct { //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#number //= type=implication - //# Number MUST be serialized as UTF-8 encoded bytes. + //# This value MUST be normalized in the same way as DynamoDB normalizes numbers. + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#number + //= type=implication + //# This normalized value MUST then be serialized as UTF-8 encoded bytes. ensures a.N? && ret.Success? && !prefix ==> && Norm.NormalizeNumber(a.N).Success? && var nn := Norm.NormalizeNumber(a.N).value; @@ -488,30 +486,52 @@ module DynamoToStruct { function method StringSetAttrToBytes(ss: StringSetAttributeValue): (ret: Result, string>) ensures ret.Success? ==> Seq.HasNoDuplicates(ss) { - :- Need(|Seq.ToSet(ss)| == |ss|, "String Set had duplicate values"); + var asSet := Seq.ToSet(ss); + :- Need(|asSet| == |ss|, "String Set had duplicate values"); Seq.LemmaNoDuplicatesCardinalityOfSet(ss); - var count :- U32ToBigEndian(|ss|); - var body :- CollectString(ss); + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //# Entries in a String Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). + var sortedList := SortedSets.ComputeSetToOrderedSequence2(asSet, CharLess); + var count :- U32ToBigEndian(|sortedList|); + var body :- CollectString(sortedList); Success(count + body) } function method NumberSetAttrToBytes(ns: NumberSetAttributeValue): (ret: Result, string>) ensures ret.Success? ==> Seq.HasNoDuplicates(ns) { - :- Need(|Seq.ToSet(ns)| == |ns|, "Number Set had duplicate values"); + var asSet := Seq.ToSet(ns); + :- Need(|asSet| == |ns|, "Number Set had duplicate values"); Seq.LemmaNoDuplicatesCardinalityOfSet(ns); - var count :- U32ToBigEndian(|ns|); - var body :- CollectString(ns); + + var normList :- Seq.MapWithResult(n => Norm.NormalizeNumber(n), ns); + var asSet := Seq.ToSet(normList); + :- Need(|asSet| == |normList|, "Number Set had duplicate values after normalization."); + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //# Entries in a Number Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //# This ordering MUST be applied after normalization of the number value. + var sortedList := SortedSets.ComputeSetToOrderedSequence2(asSet, CharLess); + var count :- U32ToBigEndian(|sortedList|); + var body :- CollectString(sortedList); Success(count + body) } function method BinarySetAttrToBytes(bs: BinarySetAttributeValue): (ret: Result, string>) ensures ret.Success? ==> Seq.HasNoDuplicates(bs) { - :- Need(|Seq.ToSet(bs)| == |bs|, "Binary Set had duplicate values"); + var asSet := Seq.ToSet(bs); + :- Need(|asSet| == |bs|, "Binary Set had duplicate values"); Seq.LemmaNoDuplicatesCardinalityOfSet(bs); - var count :- U32ToBigEndian(|bs|); - var body :- CollectBinary(bs); + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //# Entries in a Binary Set MUST be ordered lexicographically by their underlying bytes in ascending order. + var sortedList := SortedSets.ComputeSetToOrderedSequence2(asSet, ByteLess); + var count :- U32ToBigEndian(|sortedList|); + var body :- CollectBinary(sortedList); Success(count + body) } @@ -749,6 +769,9 @@ module DynamoToStruct { ensures (ret.Success? && |mapToSerialize| == 0) ==> (ret.value == serialized) ensures (ret.Success? && |mapToSerialize| == 0) ==> (|ret.value| == |serialized|) { + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-entries + //# Entries in a serialized Map MUST be ordered by key value, + //# ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). var keys := SortedSets.ComputeSetToOrderedSequence2(mapToSerialize.Keys, CharLess); CollectOrderedMapSubset(keys, mapToSerialize, serialized) } @@ -1136,6 +1159,7 @@ module DynamoToStruct { OneBadResult(m); var badValues := FlattenErrors(m); assert(|badValues| > 0); + // We happen to order these values, but this ordering MUST NOT be relied upon. var badValueSeq := SetToOrderedSequence(badValues, CharLess); Failure(Join(badValueSeq, "\n")) } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/SearchInfo.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/SearchInfo.dfy index 2032154a5..8f32308c9 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryption/src/SearchInfo.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryption/src/SearchInfo.dfy @@ -344,12 +344,6 @@ module SearchableEncryptionInfo { versions[currWrite].IsVirtualField(field) } - function method GenerateClosure(fields : seq) : seq - requires ValidState() - { - versions[currWrite].GenerateClosure(fields) - } - method GeneratePlainBeacons(item : DDB.AttributeMap) returns (output : Result) requires ValidState() { @@ -554,6 +548,7 @@ module SearchableEncryptionInfo { requires version == 1 requires keySource.ValidState() { + // We happen to order these values, but this ordering MUST NOT be relied upon. var beaconNames := SortedSets.ComputeSetToOrderedSequence2(beacons.Keys, CharLess); var stdKeys := Seq.Filter((k : string) => k in beacons && beacons[k].Standard?, beaconNames); FilterPreservesHasNoDuplicates((k : string) => k in beacons && beacons[k].Standard?, beaconNames); @@ -570,6 +565,7 @@ module SearchableEncryptionInfo { keySource : KeySource, virtualFields : VirtualFieldMap, beacons : BeaconMap, + // The ordering of `beaconNames` MUST NOT be relied upon. beaconNames : seq, stdNames : seq, encryptedFields : set @@ -609,13 +605,6 @@ module SearchableEncryptionInfo { [field] } - function method GenerateClosure(fields : seq) : seq - { - var fieldLists := Seq.Map((s : string) => GetFields(s), fields); - var fieldSet := set f <- fieldLists, g <- f :: g; - SortedSets.ComputeSetToOrderedSequence2(fieldSet, CharLess) - } - method getKeyMap(keyId : MaybeKeyId) returns (output : Result) requires ValidState() ensures ValidState() diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryption/test/DynamoToStruct.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryption/test/DynamoToStruct.dfy index c7290bb8e..ffecf051e 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryption/test/DynamoToStruct.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryption/test/DynamoToStruct.dfy @@ -282,14 +282,264 @@ module DynamoToStructTest { expect newMapValue.value == mapValue; } - //= specification/dynamodb-encryption-client/ddb-item-conversion.md#overview + method {:test} TestNormalizeNAttr() { + var numberValue := AttributeValue.N("000123.000"); + var encodedNumberData := StructuredDataTerminal(value := [49,50,51], typeId := [0,2]); + var encodedNumberValue := StructuredData(content := Terminal(encodedNumberData), attributes := None); + var numberStruct := AttrToStructured(numberValue); + expect numberStruct.Success?; + expect numberStruct.value == encodedNumberValue; + + var newNumberValue := StructuredToAttr(encodedNumberValue); + expect newNumberValue.Success?; + expect newNumberValue.value == AttributeValue.N("123"); + } + + method {:test} TestNormalizeNInSet() { + var numberSetValue := AttributeValue.NS(["001.00"]); + var encodedNumberSetData := StructuredDataTerminal(value := [0,0,0,1, 0,0,0,1, 49], typeId := [1,2]); + var encodedNumberSetValue := StructuredData(content := Terminal(encodedNumberSetData), attributes := None); + var numberSetStruct := AttrToStructured(numberSetValue); + expect numberSetStruct.Success?; + expect numberSetStruct.value == encodedNumberSetValue; + + var newNumberSetValue := StructuredToAttr(encodedNumberSetValue); + expect newNumberSetValue.Success?; + expect newNumberSetValue.value == AttributeValue.NS(["1"]); + } + + method {:test} TestNormalizeNInList() { + var nValue := AttributeValue.N("001.00"); + var normalizedNValue := AttributeValue.N("1"); + + var listValue := AttributeValue.L([nValue]); + var encodedListData := StructuredDataTerminal(value := [ + 0,0,0,1, // 1 member in list + 0,2, 0,0,0,1, 49 // 1st member is N("1") + ], + typeId := [3,0]); + var encodedListValue := StructuredData(content := Terminal(encodedListData), attributes := None); + var listStruct := AttrToStructured(listValue); + expect listStruct.Success?; + expect listStruct.value == encodedListValue; + + var newListValue := StructuredToAttr(listStruct.value); + expect newListValue.Success?; + expect newListValue.value == AttributeValue.L([normalizedNValue]); + } + + method {:test} TestNormalizeNInMap() { + var nValue := AttributeValue.N("001.00"); + var normalizedNValue := AttributeValue.N("1"); + + var mapValue := AttributeValue.M(map["keyA" := nValue]); + var k := 'k' as uint8; + var e := 'e' as uint8; + var y := 'y' as uint8; + var A := 'A' as uint8; + + var encodedMapData := StructuredDataTerminal( + value := [ + 0,0,0,1, // there is 1 entry in the map + 0,1, 0,0,0,4, k,e,y,A, // 1st entry's key + 0,2, 0,0,0,1, // 1st entry's value is a N and is 1 byte long + 49 // "1" + ], + typeId := [2,0]); + + var encodedMapValue := StructuredData(content := Terminal(encodedMapData), attributes := None); + var mapStruct := AttrToStructured(mapValue); + expect mapStruct.Success?; + expect mapStruct.value == encodedMapValue; + + var newMapValue := StructuredToAttr(mapStruct.value); + expect newMapValue.Success?; + expect newMapValue.value == AttributeValue.M(map["keyA" := normalizedNValue]); + } + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries //= type=test - //# The conversion from DDB Item to Structured Data must be lossless, - //# meaning that converting a DDB Item to - //# a Structured Data and back to a DDB Item again - //# MUST result in the exact same DDB Item. - method {:test} TestRoundTrip() { + //# Entries in a Number Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). + method {:test} TestSortNSAttr() { + var numberSetValue := AttributeValue.NS(["1","2","10"]); + var encodedNumberSetData := StructuredDataTerminal(value := [0,0,0,3, 0,0,0,1, 49, 0,0,0,2, 49,48, 0,0,0,1, 50], typeId := [1,2]); + var encodedNumberSetValue := StructuredData(content := Terminal(encodedNumberSetData), attributes := None); + var numberSetStruct := AttrToStructured(numberSetValue); + expect numberSetStruct.Success?; + expect numberSetStruct.value == encodedNumberSetValue; + + var newNumberSetValue := StructuredToAttr(encodedNumberSetValue); + expect newNumberSetValue.Success?; + expect newNumberSetValue.value == AttributeValue.NS(["1","10","2"]); + } + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=test + //# This ordering MUST be applied after normalization of the number value. + method {:test} TestSortNSAfterNormalize() { + var numberSetValue := AttributeValue.NS(["1","02","10"]); + var encodedNumberSetData := StructuredDataTerminal(value := [0,0,0,3, 0,0,0,1, 49, 0,0,0,2, 49,48, 0,0,0,1, 50], typeId := [1,2]); + var encodedNumberSetValue := StructuredData(content := Terminal(encodedNumberSetData), attributes := None); + var numberSetStruct := AttrToStructured(numberSetValue); + expect numberSetStruct.Success?; + expect numberSetStruct.value == encodedNumberSetValue; + + var newNumberSetValue := StructuredToAttr(encodedNumberSetValue); + expect newNumberSetValue.Success?; + expect newNumberSetValue.value == AttributeValue.NS(["1","10","2"]); + } + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=test + //# Entries in a String Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). + method {:test} TestSortSSAttr() { + var stringSetValue := AttributeValue.SS(["&","。","𐀂"]); + // Note that string values are UTF-8 encoded, but sorted by UTF-16 encoding. + var encodedStringSetData := StructuredDataTerminal(value := [ + 0,0,0,3, // 3 entries in set + 0,0,0,1, // 1st entry is 1 byte + 0x26, // "&" in UTF-8 encoding + 0,0,0,4, // 2nd entry is 4 bytes + 0xF0,0x90,0x80,0x82, // "𐀂" in UTF-8 encoding + 0,0,0,3, // 3rd entry is 3 bytes + 0xEF,0xBD,0xA1 // "。" in UTF-8 encoding + ], + typeId := [1,1] + ); + var encodedStringSetValue := StructuredData(content := Terminal(encodedStringSetData), attributes := None); + var stringSetStruct := AttrToStructured(stringSetValue); + expect stringSetStruct.Success?; + expect stringSetStruct.value == encodedStringSetValue; + + var newStringSetValue := StructuredToAttr(encodedStringSetValue); + expect newStringSetValue.Success?; + expect newStringSetValue.value == AttributeValue.SS(["&","𐀂","。"]); + } + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#set-entries + //= type=test + //# Entries in a Binary Set MUST be ordered lexicographically by their underlying bytes in ascending order. + method {:test} TestSortBSAttr() { + var binarySetValue := AttributeValue.BS([[1],[2],[1,0]]); + var encodedBinarySetData := StructuredDataTerminal(value := [0,0,0,3, 0,0,0,1, 1, 0,0,0,2, 1,0, 0,0,0,1, 2], typeId := [1,0xff]); + var encodedBinarySetValue := StructuredData(content := Terminal(encodedBinarySetData), attributes := None); + var binarySetStruct := AttrToStructured(binarySetValue); + expect binarySetStruct.Success?; + expect binarySetStruct.value == encodedBinarySetValue; + + var newBinarySetValue := StructuredToAttr(encodedBinarySetValue); + expect newBinarySetValue.Success?; + expect newBinarySetValue.value == AttributeValue.BS([[1],[1,0],[2]]); + } + + method {:test} TestSetsInListAreSorted() { + var nSetValue := AttributeValue.NS(["2","1","10"]); + var sSetValue := AttributeValue.SS(["&","。","𐀂"]); + var bSetValue := AttributeValue.BS([[1,0],[1],[2]]); + + var sortedNSetValue := AttributeValue.NS(["1","10","2"]); + var sortedSSetValue := AttributeValue.SS(["&","𐀂","。"]); + var sortedBSetValue := AttributeValue.BS([[1],[1,0],[2]]); + + var listValue := AttributeValue.L([nSetValue, sSetValue, bSetValue]); + var encodedListData := StructuredDataTerminal(value := [ + 0,0,0,3, // 3 members in list + 1,2, 0,0,0,20, // 1st member is a NS and is 20 bytes long + 0,0,0,3, 0,0,0,1, 49, 0,0,0,2, 49,48, 0,0,0,1, 50, // NS + 1,1, 0,0,0,24, // 2nd member is a SS and is 24 bytes long + 0,0,0,3, 0,0,0,1, 0x26, 0,0,0,4, 0xF0,0x90,0x80,0x82, 0,0,0,3, 0xEF,0xBD,0xA1, // SS + 1,0xFF, 0,0,0,20, // 3rd member is a BS and is 20 bytes long + 0,0,0,3, 0,0,0,1, 1, 0,0,0,2, 1,0, 0,0,0,1, 2 // BS + ], + typeId := [3,0]); + var encodedListValue := StructuredData(content := Terminal(encodedListData), attributes := None); + var listStruct := AttrToStructured(listValue); + expect listStruct.Success?; + expect listStruct.value == encodedListValue; + + var newListValue := StructuredToAttr(listStruct.value); + expect newListValue.Success?; + expect newListValue.value == AttributeValue.L([sortedNSetValue, sortedSSetValue, sortedBSetValue]); + } + + method {:test} TestSetsInMapAreSorted() { + var nSetValue := AttributeValue.NS(["2","1","10"]); + var sSetValue := AttributeValue.SS(["&","。","𐀂"]); + var bSetValue := AttributeValue.BS([[1,0],[1],[2]]); + var sortedNSetValue := AttributeValue.NS(["1","10","2"]); + var sortedSSetValue := AttributeValue.SS(["&","𐀂","。"]); + var sortedBSetValue := AttributeValue.BS([[1],[1,0],[2]]); + + var mapValue := AttributeValue.M(map["keyA" := sSetValue, "keyB" := nSetValue, "keyC" := bSetValue]); + var k := 'k' as uint8; + var e := 'e' as uint8; + var y := 'y' as uint8; + var A := 'A' as uint8; + var B := 'B' as uint8; + var C := 'C' as uint8; + + var encodedMapData := StructuredDataTerminal( + value := [ + 0,0,0,3, // there are 3 entries in the map + 0,1, 0,0,0,4, k,e,y,A, // 1st entry's key + 1,1, 0,0,0,24, // 1st entry's value is a SS and is 24 bytes long + 0,0,0,3, 0,0,0,1, 0x26, 0,0,0,4, 0xF0,0x90,0x80,0x82, 0,0,0,3, 0xEF,0xBD,0xA1, // SS + 0,1, 0,0,0,4, k,e,y,B, // 2nd entry's key + 1,2, 0,0,0,20, // 2nd entry's value is a NS and is 20 bytes long + 0,0,0,3, 0,0,0,1, 49, 0,0,0,2, 49,48, 0,0,0,1, 50, // NS + 0,1, 0,0,0,4, k,e,y,C, // 3rd entry's key + 1,0xFF, 0,0,0,20, // 3rd entry's value is a BS and is 20 bytes long + 0,0,0,3, 0,0,0,1, 1, 0,0,0,2, 1,0, 0,0,0,1, 2 // BS + ], + typeId := [2,0]); + + var encodedMapValue := StructuredData(content := Terminal(encodedMapData), attributes := None); + var mapStruct := AttrToStructured(mapValue); + expect mapStruct.Success?; + expect mapStruct.value == encodedMapValue; + + var newMapValue := StructuredToAttr(mapStruct.value); + expect newMapValue.Success?; + expect newMapValue.value == AttributeValue.M(map["keyA" := sortedSSetValue, "keyB" := sortedNSetValue, "keyC" := sortedBSetValue]); + } + + //= specification/dynamodb-encryption-client/ddb-attribute-serialization.md#key-value-pair-entries + //= type=test + //# Entries in a serialized Map MUST be ordered by key value, + //# ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). + method {:test} TestSortMapKeys() { + var nullValue := AttributeValue.NULL(true); + + var mapValue := AttributeValue.M(map["&" := nullValue, "。" := nullValue, "𐀂" := nullValue]); + + // Note that the string values are encoded as UTF-8, but are sorted according to UTF-16 encoding. + var encodedMapData := StructuredDataTerminal( + value := [ + 0,0,0,3, // 3 entries + 0,1, 0,0,0,1, // 1st key is a string 1 byte long + 0x26, // "&" UTF-8 encoded + 0,0, 0,0,0,0, // null value + 0,1, 0,0,0,4, // 2nd key is a string 4 bytes long + 0xF0, 0x90, 0x80, 0x82, // "𐀂" UTF-8 encoded + 0,0, 0,0,0,0, // null value + 0,1, 0,0,0,3, // 3rd key is a string 3 bytes long + 0xEF, 0xBD, 0xA1, // "。" + 0,0, 0,0,0,0 // null value + ], + typeId := [2,0]); + var encodedMapValue := StructuredData(content := Terminal(encodedMapData), attributes := None); + var mapStruct := AttrToStructured(mapValue); + expect mapStruct.Success?; + expect mapStruct.value == encodedMapValue; + + var newMapValue := StructuredToAttr(mapStruct.value); + expect newMapValue.Success?; + expect newMapValue.value == mapValue; + } + + method {:test} TestRoundTrip() { + // Note - set and number values are carefully pre-normalized. var val1 := AttributeValue.S("astring"); var val2 := AttributeValue.N("12345"); var val3 := AttributeValue.B([1,2,3,4,5]); @@ -297,7 +547,7 @@ module DynamoToStructTest { var val5 := AttributeValue.NULL(true); var val6 := AttributeValue.BS([[1,2,3,4,5],[2,3,4,5,6],[3,4,5,6,7]]); var val7 := AttributeValue.SS(["ab","cdef","ghijk"]); - var val8 := AttributeValue.NS(["1","234.567","0"]); + var val8 := AttributeValue.NS(["0", "1","234.567"]); var val9a := AttributeValue.L([val8, val7, val6]); var val9b := AttributeValue.L([val5, val4, val3]); diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy index 2042879e7..ee971d997 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy @@ -66,8 +66,79 @@ module DeleteItemTransform { method Output(config: Config, input: DeleteItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# After a [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the DeleteItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-response-Attributes). + // The response will contain Attributes if the related DeleteItem request's + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues) + // had a value of `ALL_OLD` and an item was deleted. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# The DeleteItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + // Passthrough the response if the above specification is not met + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> + output.value.transformedOutput == input.sdkOutput + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None? + { + return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + var tableConfig := config.tableEncryptionConfigs[tableName]; + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy index ed85df9ab..6ab4b6f1c 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy @@ -30,6 +30,15 @@ module DynamoDbMiddlewareSupport { .MapFailure(e => E(e)) } + // IsSigned returned whether this attribute is signed according to this config + predicate method {:opaque} IsSigned( + config : ValidTableConfig, + attr : string + ) + { + BS.IsSigned(config.itemEncryptor.config.attributeActionsOnEncrypt, attr) + } + // TestConditionExpression fails if a condition expression is not suitable for the // given encryption schema. // Generally this means no encrypted attribute is referenced. diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy index 7cec499cf..5b2e96ff9 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy @@ -83,8 +83,79 @@ module PutItemTransform { method Output(config: Config, input: PutItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# After a [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the PutItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-response-Attributes). + // The response will contain Attributes if the related PutItem request's + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues) + // had a value of `ALL_OLD` and the PutItem call replaced a pre-existing item. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# The PutItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + // Passthrough the response if the above specification is not met + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> + output.value.transformedOutput == input.sdkOutput + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None? + { + return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + var tableConfig := config.tableEncryptionConfigs[tableName]; + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy index 0e66fba1d..6e51ec902 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy @@ -65,8 +65,142 @@ module UpdateItemTransform { method Output(config: Config, input: UpdateItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# After a [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the UpdateItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-response-Attributes). + // - the original UpdateItem request had a + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues) + // with a value of `ALL_OLD` or `ALL_NEW`. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + && input.originalInput.ReturnValues.Some? + && ( + || input.originalInput.ReturnValues.value.ALL_OLD? + || input.originalInput.ReturnValues.value.ALL_NEW? + ) + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# The UpdateItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# In all other cases, the UpdateItem response MUST NOT be modified. + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> ( + && output.value.transformedOutput == input.sdkOutput + ) + + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + && (input.originalInput.ReturnValues.Some? ==> ( + || input.originalInput.ReturnValues.value.UPDATED_NEW? + || input.originalInput.ReturnValues.value.UPDATED_OLD? + ) + ) + ) ==> ( + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && output.value.transformedOutput == input.sdkOutput + && forall k <- input.sdkOutput.Attributes.value.Keys :: !IsSigned(tableConfig, k) + ) + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# Additionally, if a value of `UPDATED_OLD` or `UPDATED_NEW` was used, + //# and any Attributes in the response are authenticated + //# per the [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration), + //# an error MUST be raised. + ensures ( + && input.originalInput.TableName in config.tableEncryptionConfigs + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && input.sdkOutput.Attributes.Some? + && (input.originalInput.ReturnValues.Some? ==> ( + || input.originalInput.ReturnValues.value.UPDATED_NEW? + || input.originalInput.ReturnValues.value.UPDATED_OLD? + ) + ) + && exists k <- input.sdkOutput.Attributes.value.Keys :: IsSigned(tableConfig, k) + ) ==> + output.Failure? + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + + if + || tableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + { + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + + var tableConfig := config.tableEncryptionConfigs[tableName]; + var attributes := input.sdkOutput.Attributes.value; + + if !( + && input.originalInput.ReturnValues.Some? + && ( + || input.originalInput.ReturnValues.value.ALL_NEW? + || input.originalInput.ReturnValues.value.ALL_OLD?) + ) + { + // This error should not be possible to reach if we assume the DDB API contract is correct. + // We include this runtime check for defensive purposes. + :- Need(forall k <- attributes.Keys :: !IsSigned(tableConfig, k), + E("UpdateItems response contains signed attributes, but does not include the entire item which is required for verification.")); + + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=attributes) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations.dfy b/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations.dfy index 7d56fd9cd..5ea299388 100644 --- a/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations.dfy @@ -525,6 +525,7 @@ module AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorOperations refines Abs function method GetItemNames(item : ComAmazonawsDynamodbTypes.AttributeMap) : string { + // We happen to order these values, but this ordering MUST NOT be relied upon. var keys := SortedSets.ComputeSetToOrderedSequence2(item.Keys, CharLess); if |keys| == 0 then "item is empty" diff --git a/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy b/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy index d226f16ac..9f88ab83e 100644 --- a/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbItemEncryptor/src/Index.dfy @@ -93,6 +93,7 @@ module message := "Sort key attribute action MUST be SIGN_ONLY" )); + // We happen to order these values, but this ordering MUST NOT be relied upon. var attributeNames : seq := SortedSets.ComputeSetToOrderedSequence2(config.attributeActionsOnEncrypt.Keys, CharLess); for i := 0 to |attributeNames| invariant forall j | 0 <= j < i :: diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 1519c07b6..10009eef3 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "software.amazon.cryptography" -version = "3.1.0" +version = "3.1.2" description = "Aws Database Encryption Sdk for DynamoDb Java" java { diff --git a/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java b/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java index 78e8848be..bdfa8306a 100644 --- a/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java +++ b/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java @@ -252,6 +252,17 @@ public SdkResponse modifyResponse(Context.ModifyResponse context, ExecutionAttri .sdkHttpResponse(originalResponse.sdkHttpResponse()) .build(); break; + } case "DeleteItem": { + DeleteItemResponse transformedResponse = transformer.DeleteItemOutputTransform( + DeleteItemOutputTransformInput.builder() + .sdkOutput((DeleteItemResponse) originalResponse) + .originalInput((DeleteItemRequest) originalRequest) + .build()).transformedOutput(); + outgoingResponse = transformedResponse.toBuilder() + .responseMetadata(((DeleteItemResponse) originalResponse).responseMetadata()) + .sdkHttpResponse(originalResponse.sdkHttpResponse()) + .build(); + break; } case "ExecuteStatement": { ExecuteStatementResponse transformedResponse = transformer.ExecuteStatementOutputTransform( ExecuteStatementOutputTransformInput.builder() diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java index 45f490968..a6c11bdba 100644 --- a/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java @@ -21,6 +21,9 @@ import java.lang.annotation.Target; /** + * Warning: This annotation only works with the DynamoDBMapper for AWS SDK for Java 1.x. + * If you are using the AWS SDK for Java 2.x, use @DynamoDbEncryptionSignOnly instead. + * * Prevents the associated item (class or attribute) from being encrypted. * *

For guidance on performing a safe data model change procedure, please see For guidance on performing a safe data model change procedure, please see putItem = putResponse.attributes(); + assertNotNull(putItem); + assertEquals(partitionValue, putItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, putItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, putItem.get(TEST_ATTR_NAME).s()); // Get Item back from table Map keyToGet = createTestKey(partitionValue, sortValue); @@ -87,6 +99,147 @@ public void TestPutItemGetItem() { assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); } + // Test that if we update a DO_NOTHING attribute, + // we correctly decrypt the ALL_* return values + @Test + public void TestUpdateItemReturnAll() { + // Put item into table + String partitionValue = "update_ALL"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + ddbKmsKeyring.putItem(putRequest); + + // Update unsigned attribute, and return ALL_OLD + Map keyToGet = createTestKey(partitionValue, sortValue); + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .key(keyToGet) + .returnValues(ReturnValue.ALL_OLD) + .updateExpression("SET #D = :d") + .expressionAttributeNames(Collections.singletonMap("#D", "attr2")) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated"))) + .tableName(TEST_TABLE_NAME) + .build(); + + UpdateItemResponse updateResponse = ddbKmsKeyring.updateItem(updateRequest); + assertEquals(200, updateResponse.sdkHttpResponse().statusCode()); + Map returnedItem = updateResponse.attributes(); + assertNotNull(returnedItem); + assertEquals(partitionValue, returnedItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); + assertEquals("hello world", returnedItem.get(TEST_ATTR2_NAME).s()); + + // Update unsigned attribute, and return ALL_NEW + UpdateItemRequest updateRequest2 = updateRequest.toBuilder() + .returnValues(ReturnValue.ALL_NEW) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated2"))) + .build(); + + UpdateItemResponse updateResponse2 = ddbKmsKeyring.updateItem(updateRequest2); + assertEquals(200, updateResponse2.sdkHttpResponse().statusCode()); + Map returnedItem2 = updateResponse2.attributes(); + assertNotNull(returnedItem2); + assertEquals(partitionValue, returnedItem2.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem2.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem2.get(TEST_ATTR_NAME).s()); + assertEquals("updated2", returnedItem2.get(TEST_ATTR2_NAME).s()); + } + + // Test that if we update a DO_NOTHING attribute, + // we correctly pass through the UPDATED_* return values + @Test + public void TestUpdateItemReturnUpdated() { + // Put item into table + String partitionValue = "update_UPDATE"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + ddbKmsKeyring.putItem(putRequest); + + // Update unsigned attribute, and return UPDATED_OLD + Map keyToGet = createTestKey(partitionValue, sortValue); + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .key(keyToGet) + .returnValues(ReturnValue.UPDATED_OLD) + .updateExpression("SET #D = :d") + .expressionAttributeNames(Collections.singletonMap("#D", "attr2")) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated"))) + .tableName(TEST_TABLE_NAME) + .build(); + + UpdateItemResponse updateResponse = ddbKmsKeyring.updateItem(updateRequest); + assertEquals(200, updateResponse.sdkHttpResponse().statusCode()); + Map returnedItem = updateResponse.attributes(); + assertNotNull(returnedItem); + assertFalse(returnedItem.containsKey(TEST_ATTR_NAME)); + assertEquals("hello world", returnedItem.get(TEST_ATTR2_NAME).s()); + + // Update unsigned attribute, and return ALL_NEW + UpdateItemRequest updateRequest2 = updateRequest.toBuilder() + .returnValues(ReturnValue.UPDATED_NEW) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated2"))) + .build(); + + UpdateItemResponse updateResponse2 = ddbKmsKeyring.updateItem(updateRequest2); + assertEquals(200, updateResponse2.sdkHttpResponse().statusCode()); + Map returnedItem2 = updateResponse2.attributes(); + assertNotNull(returnedItem2); + assertFalse(returnedItem2.containsKey(TEST_ATTR_NAME)); + assertEquals("updated2", returnedItem2.get(TEST_ATTR2_NAME).s()); + } + + @Test + public void TestDeleteItem() { + // Put item into table + String partitionValue = "delete"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .build(); + + PutItemResponse putResponse = ddbKmsKeyring.putItem(putRequest); + assertEquals(200, putResponse.sdkHttpResponse().statusCode()); + + // Delete item from table, set ReturnValues to ALL_OLD to return deleted item + Map keyToGet = createTestKey(partitionValue, sortValue); + + DeleteItemRequest deleteRequest = DeleteItemRequest.builder() + .key(keyToGet) + .tableName(TEST_TABLE_NAME) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + DeleteItemResponse deleteResponse = ddbKmsKeyring.deleteItem(deleteRequest); + assertEquals(200, deleteResponse.sdkHttpResponse().statusCode()); + Map returnedItem = deleteResponse.attributes(); + assertNotNull(returnedItem); + assertEquals(partitionValue, returnedItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); + } + @Test public void TestBatchWriteBatchGet() { // Batch write items to table diff --git a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java index 207fb41f4..ae84494ba 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java +++ b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java @@ -15,8 +15,12 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; import software.amazon.awssdk.services.kms.model.KmsException; import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels.*; @@ -31,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; import static org.testng.Assert.assertEquals; import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.*; @@ -41,6 +46,11 @@ public class DynamoDbEncryptionEnhancedClientIntegrationTests { + // Some integration tests MUST mutate the state of the DDB table. + // For such tests, include a random number in the primary key + // to avoid conflicts between distributed test runners sharing a table. + private int randomNum = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE ); + private static DynamoDbEnhancedClient initEnhancedClientWithInterceptor( final TableSchema schemaOnEncrypt, final List allowedUnsignedAttributes, @@ -75,6 +85,46 @@ private static DynamoDbEnhancedClient initEnhancedClientWithInterceptor( .build(); } + private static DynamoDbEnhancedClient createEnhancedClientForLegacyClass(DynamoDBEncryptor oldEncryptor, TableSchema schemaOnEncrypt) { + Map legacyActions = new HashMap<>(); + legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); + legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); + legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); + legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); + legacyActions.put("doNothing", CryptoAction.DO_NOTHING); + LegacyOverride legacyOverride = LegacyOverride + .builder() + .encryptor(oldEncryptor) + .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) + .attributeActionsOnEncrypt(legacyActions) + .build(); + + Map tableConfigs = new HashMap<>(); + tableConfigs.put(TEST_TABLE_NAME, + DynamoDbEnhancedTableEncryptionConfig.builder() + .logicalTableName(TEST_TABLE_NAME) + .keyring(createKmsKeyring()) + .allowedUnsignedAttributes(Arrays.asList("doNothing")) + .schemaOnEncrypt(schemaOnEncrypt) + .legacyOverride(legacyOverride) + .build()); + DynamoDbEncryptionInterceptor interceptor = + DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor( + CreateDynamoDbEncryptionInterceptorInput.builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ); + DynamoDbClient ddb = DynamoDbClient.builder() + .overrideConfiguration( + ClientOverrideConfiguration.builder() + .addExecutionInterceptor(interceptor) + .build()) + .build(); + return DynamoDbEnhancedClient.builder() + .dynamoDbClient(ddb) + .build(); + } + @Test public void TestPutAndGet() { TableSchema schemaOnEncrypt = TableSchema.fromBean(SimpleClass.class); @@ -109,6 +159,35 @@ public void TestPutAndGet() { assertEquals(result.getDoNothing(), "fizzbuzz"); } + @Test + public void TestPutAndGetAllTypes() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); + + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedPutGetAllTypes", 1); + + // Put an item into DDB such that it also returns back the item. + PutItemEnhancedResponse putItemResp = table.putItemWithResponse( + (PutItemEnhancedRequest.Builder requestBuilder) + -> requestBuilder.item(record) + .returnValues(ReturnValue.ALL_OLD)); + assertEquals(putItemResp.attributes(), record); + + // Get the item back from the table + Key key = Key.builder() + .partitionValue("EnhancedPutGetAllTypes").sortValue(1) + .build(); + + // Get the item by using the key. + AllTypesClass result = table.getItem( + (GetItemEnhancedRequest.Builder requestBuilder) -> requestBuilder.key(key)); + assertEquals(result, record); + } + @Test public void TestPutAndGetAnnotatedFlattenedBean() { final String PARTITION = "AnnotatedFlattenedBean"; @@ -236,20 +315,6 @@ public void TestGetLegacyItem() { mapper.save(record); - // Configure EnhancedClient with Legacy behavior - Map legacyActions = new HashMap<>(); - legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); - legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); - legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); - legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); - legacyActions.put("doNothing", CryptoAction.DO_NOTHING); - LegacyOverride legacyOverride = LegacyOverride - .builder() - .encryptor(oldEncryptor) - .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) - .attributeActionsOnEncrypt(legacyActions) - .build(); - TableSchema schemaOnEncrypt = TableSchema.fromBean(LegacyClass.class); DynamoDbEnhancedClient enhancedClient = createEnhancedClientForLegacyClass(oldEncryptor, schemaOnEncrypt); @@ -300,44 +365,58 @@ public void TestWriteLegacyItem() { assertEquals("fizzbuzz", result.getDoNothing()); } - private static DynamoDbEnhancedClient createEnhancedClientForLegacyClass(DynamoDBEncryptor oldEncryptor, TableSchema schemaOnEncrypt) { - Map legacyActions = new HashMap<>(); - legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); - legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); - legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); - legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); - legacyActions.put("doNothing", CryptoAction.DO_NOTHING); - LegacyOverride legacyOverride = LegacyOverride - .builder() - .encryptor(oldEncryptor) - .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) - .attributeActionsOnEncrypt(legacyActions) - .build(); + @Test + public void TestDelete() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); - Map tableConfigs = new HashMap<>(); - tableConfigs.put(TEST_TABLE_NAME, - DynamoDbEnhancedTableEncryptionConfig.builder() - .logicalTableName(TEST_TABLE_NAME) - .keyring(createKmsKeyring()) - .allowedUnsignedAttributes(Arrays.asList("doNothing")) - .schemaOnEncrypt(schemaOnEncrypt) - .legacyOverride(legacyOverride) - .build()); - DynamoDbEncryptionInterceptor interceptor = - DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor( - CreateDynamoDbEncryptionInterceptorInput.builder() - .tableEncryptionConfigs(tableConfigs) - .build() - ); - DynamoDbClient ddb = DynamoDbClient.builder() - .overrideConfiguration( - ClientOverrideConfiguration.builder() - .addExecutionInterceptor(interceptor) - .build()) - .build(); - return DynamoDbEnhancedClient.builder() - .dynamoDbClient(ddb) + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedDelete", randomNum); + + // Put an item into an Amazon DynamoDB table. + table.putItem(record); + + // Get the item back from the table + Key key = Key.builder() + .partitionValue("EnhancedDelete").sortValue(randomNum) .build(); + + // Get the item by using the key. + AllTypesClass result = table.deleteItem(key); + assertEquals(result, record); + } + + @Test + public void TestUpdate() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); + + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedUpdate", 1); + + // Put an item into an Amazon DynamoDB table. + table.putItem(record); + + AllTypesClass doNothingValue = new AllTypesClass(); + doNothingValue.setDoNothing("updatedDoNothing"); + doNothingValue.setPartitionKey("EnhancedUpdate"); + doNothingValue.setSortKey(1); + + // Perform an update only on "doNothing" attribute + AllTypesClass result = table.updateItem( + (UpdateItemEnhancedRequest.Builder requestBuilder) + -> requestBuilder.item(doNothingValue) + .ignoreNulls(true) + ); + // EnhancedClient uses ReturnValues of ALL_NEW, so compare against put item with update + record.setDoNothing("updatedDoNothing"); + assertEquals(result, record); } @Test( diff --git a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java new file mode 100644 index 000000000..ef50fd2fe --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java @@ -0,0 +1,386 @@ +package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionDoNothing; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionSignOnly; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class is used by the Enhanced Client Tests + */ + +@DynamoDbBean +public class AllTypesClass { + + private String partitionKey; + private int sortKey; + + // One attribute for every DDB attribute type and ENCRYPT_AND_SIGN/SIGN_ONLY pair + private String encryptString; + private String signString; + private Double encryptNum; + private Double signNum; + private ByteBuffer encryptBinary; + private ByteBuffer signBinary; + private Boolean encryptBool; + private Boolean signBool; + private String encryptExpectedNull; // This should always be null, define no setters + private String signExpectedNull; // This should always be null, define no setters + private List encryptList; + private List signList; + private Map encryptMap; + private Map signMap; + private Set encryptStringSet; + private Set signStringSet; + private Set encryptNumSet; + private Set signNumSet; + private Set encryptBinarySet; + private Set signBinarySet; + + // And one doNothing for good measure + private String doNothing; + + @DynamoDbPartitionKey + @DynamoDbAttribute(value = "partition_key") + public String getPartitionKey() { + return this.partitionKey; + } + + @DynamoDbSortKey + @DynamoDbAttribute(value = "sort_key") + public int getSortKey() { + return this.sortKey; + } + + @DynamoDbIgnoreNulls + public String getEncryptString() { + return this.encryptString; + } + + @DynamoDbEncryptionSignOnly + @DynamoDbIgnoreNulls + public String getSignString() { + return this.signString; + } + + @DynamoDbIgnoreNulls + public Double getEncryptNum() { + return encryptNum; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Double getSignNum() { + return signNum; + } + + @DynamoDbIgnoreNulls + public ByteBuffer getEncryptBinary() { + return encryptBinary; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public ByteBuffer getSignBinary() { + return signBinary; + } + + @DynamoDbIgnoreNulls + public Boolean getEncryptBool() { + return encryptBool; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Boolean getSignBool() { + return signBool; + } + + // This should always return null + public String getEncryptExpectedNull() { + return encryptExpectedNull; + } + + // This should always return null + @DynamoDbEncryptionSignOnly + public String getSignExpectedNull() { + return signExpectedNull; + } + + @DynamoDbIgnoreNulls + public List getEncryptList() { + return encryptList; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public List getSignList() { + return signList; + } + + @DynamoDbIgnoreNulls + public Map getEncryptMap() { + return encryptMap; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Map getSignMap() { + return signMap; + } + + @DynamoDbIgnoreNulls + public Set getEncryptStringSet() { + return encryptStringSet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignStringSet() { + return signStringSet; + } + + @DynamoDbIgnoreNulls + public Set getEncryptNumSet() { + return encryptNumSet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignNumSet() { + return signNumSet; + } + + @DynamoDbIgnoreNulls + public Set getEncryptBinarySet() { + return encryptBinarySet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignBinarySet() { + return signBinarySet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionDoNothing + public String getDoNothing() { + return this.doNothing; + } + + public void setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + } + + public void setSortKey(int sortKey) { + this.sortKey = sortKey; + } + + public void setEncryptString(String encryptString) { + this.encryptString = encryptString; + } + + public void setSignString(String signString) { + this.signString = signString; + } + + public void setEncryptNum(Double encryptNum) { + this.encryptNum = encryptNum; + } + + public void setSignNum(Double signNum) { + this.signNum = signNum; + } + + public void setEncryptBinary(ByteBuffer encryptBinary) { + this.encryptBinary = encryptBinary; + } + + public void setSignBinary(ByteBuffer signBinary) { + this.signBinary = signBinary; + } + + public void setEncryptBool(Boolean encryptBool) { + this.encryptBool = encryptBool; + } + + public void setSignBool(Boolean signBool) { + this.signBool = signBool; + } + + public void setEncryptList(List encryptList) { + this.encryptList = encryptList; + } + + public void setSignList(List signList) { + this.signList = signList; + } + + public void setEncryptMap(Map encryptMap) { + this.encryptMap = encryptMap; + } + + public void setSignMap(Map signMap) { + this.signMap = signMap; + } + + public void setEncryptStringSet(Set encryptStringSet) { + this.encryptStringSet = encryptStringSet; + } + + public void setSignStringSet(Set signStringSet) { + this.signStringSet = signStringSet; + } + + public void setEncryptNumSet(Set encryptNumSet) { + this.encryptNumSet = encryptNumSet; + } + + public void setSignNumSet(Set signNumSet) { + this.signNumSet = signNumSet; + } + + public void setEncryptBinarySet(Set encryptBinarySet) { + this.encryptBinarySet = encryptBinarySet; + } + + public void setSignBinarySet(Set signBinarySet) { + this.signBinarySet = signBinarySet; + } + + public void setDoNothing(String doNothing) { + this.doNothing = doNothing; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (obj.getClass() != this.getClass()) { + return false; + } + + final AllTypesClass other = (AllTypesClass) obj; + + if (!(Objects.equals(other.getPartitionKey(), this.partitionKey))) { + return false; + } + if (other.getSortKey() != this.sortKey) { + return false; + } + if (!(Objects.equals(other.getEncryptString(), this.encryptString))) { + return false; + } + if (!(Objects.equals(other.getSignString(), this.signString))) { + return false; + } + if (!(Objects.equals(other.getEncryptNum(), this.encryptNum))) { + return false; + } + if (!(Objects.equals(other.getSignNum(), this.signNum))) { + return false; + } + if (!(Objects.equals(other.getEncryptBinary(), this.encryptBinary))) { + return false; + } + if (!(Objects.equals(other.getSignBinary(), this.signBinary))) { + return false; + } + if (!(Objects.equals(other.getEncryptBool(), this.encryptBool))) { + return false; + } + if (!(Objects.equals(other.getSignBool(), this.signBool))) { + return false; + } + if (other.getEncryptExpectedNull() != null) { + return false; + } + if (other.getSignExpectedNull() != null) { + return false; + } + if (!(Objects.equals(other.getEncryptList(), this.encryptList))) { + return false; + } + if (!(Objects.equals(other.getSignList(), this.signList))) { + return false; + } + if (!(Objects.equals(other.getEncryptMap(), this.encryptMap))) { + return false; + } + if (!(Objects.equals(other.getSignMap(), this.signMap))) { + return false; + } + if (!(Objects.equals(other.getEncryptStringSet(), this.encryptStringSet))) { + return false; + } + if (!(Objects.equals(other.getSignStringSet(), this.signStringSet))) { + return false; + } + if (!(Objects.equals(other.getEncryptNumSet(), this.encryptNumSet))) { + return false; + } + if (!(Objects.equals(other.getSignNumSet(), this.signNumSet))) { + return false; + } + if (!(Objects.equals(other.getEncryptBinarySet(), this.encryptBinarySet))) { + return false; + } + if (!(Objects.equals(other.getSignBinarySet(), this.signBinarySet))) { + return false; + } + if (!(Objects.equals(other.getDoNothing(), this.doNothing))) { + return false; + } + + return true; + } + + public static AllTypesClass createTestItem(String partitionValue, int sortValue) { + AllTypesClass testItem = new AllTypesClass(); + testItem.setPartitionKey(partitionValue); + testItem.setSortKey(sortValue); + testItem.setEncryptString("encryptString"); + testItem.setSignString("signString"); + testItem.setEncryptNum(111.111); + testItem.setSignNum(999.999); + testItem.setEncryptBinary(StandardCharsets.UTF_8.encode("encryptBinary")); + testItem.setSignBinary(StandardCharsets.UTF_8.encode("sortBinary")); + testItem.setEncryptBool(true); + testItem.setSignBool(false); + testItem.setEncryptList(Arrays.asList("encrypt1", "encrypt2", "encrypt3")); + testItem.setSignList(Arrays.asList("sort1", "sort2", "sort3")); + testItem.setEncryptMap(Collections.singletonMap("encryptMap", 1)); + testItem.setSignMap(Collections.singletonMap("sortMap", 2)); + testItem.setEncryptStringSet(new HashSet<>(Arrays.asList("encrypt1", "encrypt2", "encrypt3"))); + testItem.setSignStringSet(new HashSet<>(Arrays.asList("sort1", "sort2", "sort3"))); + testItem.setEncryptNumSet(new HashSet<>(Arrays.asList(1, 2, 3))); + testItem.setSignNumSet(new HashSet<>(Arrays.asList(4, 5, 6))); + testItem.setEncryptBinarySet(new HashSet<>(Arrays.asList( + StandardCharsets.UTF_8.encode("encrypt1"), + StandardCharsets.UTF_8.encode("encrypt2"), + StandardCharsets.UTF_8.encode("encrypt3") + ))); + testItem.setSignBinarySet(new HashSet<>(Arrays.asList( + StandardCharsets.UTF_8.encode("sort1"), + StandardCharsets.UTF_8.encode("sort2"), + StandardCharsets.UTF_8.encode("sort3") + ))); + testItem.setDoNothing("doNothing"); + return testItem; + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts b/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts index 8c69ee14a..a901e27c7 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts +++ b/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts @@ -57,7 +57,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.0") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/CompoundBeaconSearchableEncryptionExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/CompoundBeaconSearchableEncryptionExample.java index 062057e53..82a7ae127 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/CompoundBeaconSearchableEncryptionExample.java +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/CompoundBeaconSearchableEncryptionExample.java @@ -298,16 +298,29 @@ public static void PutAndQueryItemWithCompoundBeacon(DynamoDbClient ddb, String .expressionAttributeValues(expressionAttributeValues) .build(); - QueryResponse queryResponse = ddb.query(queryRequest); - List> attributeValues = queryResponse.items(); - // Validate query was returned successfully - assert 200 == queryResponse.sdkHttpResponse().statusCode(); - // Validate only 1 item was returned: the item we just put - assert attributeValues.size() == 1; - Map returnedItem = attributeValues.get(0); - // Validate the item has the expected attributes - assert returnedItem.get("inspector_id_last4").s().equals("5678"); - assert returnedItem.get("unit").s().equals("011899988199"); + // GSIs do not update instantly + // so if the results come back empty + // we retry after a short sleep + for (int i=0; i<10; ++i) { + QueryResponse queryResponse = ddb.query(queryRequest); + List> attributeValues = queryResponse.items(); + // Validate query was returned successfully + assert 200 == queryResponse.sdkHttpResponse().statusCode(); + + // if no results, sleep and try again + if (attributeValues.size() == 0) { + try {Thread.sleep(20);} catch (Exception e) {} + continue; + } + + // Validate only 1 item was returned: the item we just put + assert attributeValues.size() == 1; + Map returnedItem = attributeValues.get(0); + // Validate the item has the expected attributes + assert returnedItem.get("inspector_id_last4").s().equals("5678"); + assert returnedItem.get("unit").s().equals("011899988199"); + break; + } } public static void main(final String[] args) { diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/VirtualBeaconSearchableEncryptionExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/VirtualBeaconSearchableEncryptionExample.java index 49f1f7d79..e65cf91de 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/VirtualBeaconSearchableEncryptionExample.java +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/searchableencryption/VirtualBeaconSearchableEncryptionExample.java @@ -407,16 +407,29 @@ public static void PutItemQueryItemWithVirtualBeacon(String ddbTableName, String .expressionAttributeValues(expressionAttributeValues) .build(); - final QueryResponse queryResponse = ddb.query(queryRequest); - List> attributeValues = queryResponse.items(); - // Validate query was returned successfully - assert 200 == queryResponse.sdkHttpResponse().statusCode(); - // Validate only 1 item was returned: the item with the expected attributes - assert attributeValues.size() == 1; - final Map returnedItem = attributeValues.get(0); - // Validate the item has the expected attributes - assert returnedItem.get("state").s().equals("CA"); - assert returnedItem.get("hasTestResult").bool().equals(true); + // GSIs do not update instantly + // so if the results come back empty + // we retry after a short sleep + for (int i=0; i<10; ++i) { + final QueryResponse queryResponse = ddb.query(queryRequest); + List> attributeValues = queryResponse.items(); + // Validate query was returned successfully + assert 200 == queryResponse.sdkHttpResponse().statusCode(); + + // if no results, sleep and try again + if (attributeValues.size() == 0) { + try {Thread.sleep(20);} catch (Exception e) {} + continue; + } + + // Validate only 1 item was returned: the item with the expected attributes + assert attributeValues.size() == 1; + final Map returnedItem = attributeValues.get(0); + // Validate the item has the expected attributes + assert returnedItem.get("state").s().equals("CA"); + assert returnedItem.get("hasTestResult").bool().equals(true); + break; + } } public static void main(final String[] args) { diff --git a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index 8c374d8eb..11e4a65dc 100644 --- a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts @@ -56,7 +56,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.0") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts index 6d040d4ad..26ba74824 100644 --- a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts @@ -56,7 +56,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.0") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/README.md b/README.md index cfbd48588..5441851b3 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ To use the DB-ESDK for DynamoDB in Java, you must have: * **Via Gradle Kotlin** In a Gradle Java Project, add the following to the _dependencies_ section: ```kotlin - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.0") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) implementation("software.amazon.awssdk:dynamodb") @@ -92,7 +92,7 @@ To use the DB-ESDK for DynamoDB in Java, you must have: software.amazon.cryptography aws-database-encryption-sdk-dynamodb - 3.1.0 + 3.1.1 software.amazon.cryptography diff --git a/TestVectors/dafny/DDBEncryption/src/Index.dfy b/TestVectors/dafny/DDBEncryption/src/Index.dfy index a982f81c1..8a67f6039 100644 --- a/TestVectors/dafny/DDBEncryption/src/Index.dfy +++ b/TestVectors/dafny/DDBEncryption/src/Index.dfy @@ -3,11 +3,13 @@ include "LibraryIndex.dfy" include "TestVectors.dfy" +include "WriteSetPermutations.dfy" module WrappedDDBEncryptionMain { import opened Wrappers import opened DdbEncryptionTestVectors + import WriteSetPermutations import CreateInterceptedDDBClient import FileIO import JSON.API @@ -25,11 +27,13 @@ module WrappedDDBEncryptionMain { } method ASDF() { + WriteSetPermutations.WriteSetPermutations(); var config := MakeEmptyTestVector(); config :- expect AddJson(config, "records.json"); config :- expect AddJson(config, "configs.json"); config :- expect AddJson(config, "data.json"); config :- expect AddJson(config, "iotest.json"); + config :- expect AddJson(config, "PermTest.json"); config.RunAllTests(); } } diff --git a/TestVectors/dafny/DDBEncryption/src/Permute.dfy b/TestVectors/dafny/DDBEncryption/src/Permute.dfy new file mode 100644 index 000000000..1c030a2f3 --- /dev/null +++ b/TestVectors/dafny/DDBEncryption/src/Permute.dfy @@ -0,0 +1,72 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +include "../../../../submodules/MaterialProviders/libraries/src/Wrappers.dfy" + +module {:options "-functionSyntax:4"} Permutations { + import opened Wrappers + + method GeneratePermutations(source : seq) returns (result : seq>) + { + if |source| == 0 { + return []; + } + if |source| == 1 { + return [source]; + } + var A := new T[|source|](i requires 0 <= i < |source| => source[i]); + result := Permute(A.Length, A); + } + + method Swap(A : array, x : nat, y : nat) + requires 0 <= x < A.Length + requires 0 <= y < A.Length + modifies A + { + var tmp := A[x]; + A[x] := A[y]; + A[y] := tmp; + } + + // https://en.wikipedia.org/wiki/Heap%27s_algorithm + // Each step generates the k! permutations that end with the same n-k final elements + method Permute(k : nat, A : array) returns (result : seq>) + requires 0 < k <= A.Length + modifies A + { + if k == 1 { + return [A[..]]; + } else { + var result : seq> := []; + for i := 0 to k { + var next := Permute(k - 1, A); + result := result + next; + if (k % 1) == 0 { + Swap(A, i, k-1); + } else { + Swap(A, 0, k-1); + } + } + return result; + } + } + + method {:test} BasicTests() { + var zero := GeneratePermutations([]); + var one := GeneratePermutations([1]); + var two := GeneratePermutations([1,2]); + var three := GeneratePermutations([1,2,3]); + var four := GeneratePermutations([1,2,3,4]); + expect zero == []; + expect one == [[1]]; + expect two == [[1,2],[2,1]]; + expect three == [[1, 2, 3], [2, 1, 3], [3, 1, 2], [1, 3, 2], [1, 2, 3], [2, 1, 3]]; + expect four == [ + [1, 2, 3, 4], [2, 1, 3, 4], [3, 1, 2, 4], [1, 3, 2, 4], [1, 2, 3, 4], [2, 1, 3, 4], + [4, 1, 3, 2], [1, 4, 3, 2], [3, 4, 1, 2], [4, 3, 1, 2], [4, 1, 3, 2], [1, 4, 3, 2], + [1, 2, 3, 4], [2, 1, 3, 4], [3, 1, 2, 4], [1, 3, 2, 4], [1, 2, 3, 4], [2, 1, 3, 4], + [2, 1, 4, 3], [1, 2, 4, 3], [4, 2, 1, 3], [2, 4, 1, 3], [2, 1, 4, 3], [1, 2, 4, 3] + ]; + } +} + diff --git a/TestVectors/dafny/DDBEncryption/src/TestVectors.dfy b/TestVectors/dafny/DDBEncryption/src/TestVectors.dfy index 4baa2452c..d4ffef82d 100644 --- a/TestVectors/dafny/DDBEncryption/src/TestVectors.dfy +++ b/TestVectors/dafny/DDBEncryption/src/TestVectors.dfy @@ -38,6 +38,8 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { import KeyVectorsTypes = AwsCryptographyMaterialProvidersTestVectorKeysTypes import KeyVectors import CreateInterceptedDDBClient + import SortedSets + import Seq predicate IsValidInt32(x: int) { -0x8000_0000 <= x < 0x8000_0000} type ConfigName = string @@ -72,6 +74,11 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { failures : seq ) + datatype RoundTripTest = RoundTripTest ( + configs : map, + records : seq + ) + datatype WriteTest = WriteTest ( config : TableConfig, records : seq, @@ -112,6 +119,7 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { configsForIoTest : PairList, configsForModTest : PairList, writeTests : seq, + roundTripTests : seq, decryptTests : seq ) { @@ -128,6 +136,12 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { print |ioTests|, " ioTests.\n"; print |configsForIoTest|, " configsForIoTest.\n"; print |configsForModTest|, " configsForModTest.\n"; + if |roundTripTests| != 0 { + print |roundTripTests[0].configs|, " configs and ", |roundTripTests[0].records|, " records for round trip.\n"; + } + if |roundTripTests| > 1 { + print |roundTripTests[1].configs|, " configs and ", |roundTripTests[1].records|, " records for round trip.\n"; + } Validate(); BasicIoTest(); RunIoTests(); @@ -135,11 +149,13 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { ConfigModTest(); ComplexTests(); WriteTests(); + RoundTripTests(); DecryptTests(); var client :- expect CreateInterceptedDDBClient.CreateVanillaDDBClient(); DeleteTable(client); } + method Validate() { var bad := false; for i := 0 to |globalRecords| { @@ -495,6 +511,57 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { } } + method RoundTripTests() + { + print "RoundTripTests\n"; + for i := 0 to |roundTripTests| { + + var configs := roundTripTests[i].configs; + var records := roundTripTests[i].records; + var keys := SortedSets.ComputeSetToOrderedSequence2(configs.Keys, CharLess); + + for j := 0 to |keys| { + var client :- expect newGazelle(configs[keys[j]]); + for k := 0 to |records| { + OneRoundTripTest(client, records[k]); + } + } + } + } + + method OneRoundTripTest(client : DDB.IDynamoDBClient, record : Record) { + var putInput := DDB.PutItemInput( + TableName := TableName, + Item := record.item, + Expected := None, + ReturnValues := None, + ReturnConsumedCapacity := None, + ReturnItemCollectionMetrics := None, + ConditionalOperator := None, + ConditionExpression := None, + ExpressionAttributeNames := None, + ExpressionAttributeValues := None + ); + var _ :- expect client.PutItem(putInput); + + var getInput := DDB.GetItemInput( + TableName := TableName, + Key := map[HashName := record.item[HashName]], + AttributesToGet := None, + ConsistentRead := None, + ReturnConsumedCapacity := None, + ProjectionExpression := None, + ExpressionAttributeNames := None + ); + var out :- expect client.GetItem(getInput); + expect out.Item.Some?; + if NormalizeItem(out.Item.value) != NormalizeItem(record.item) { + print "\n", NormalizeItem(out.Item.value), "\n", NormalizeItem(record.item), "\n"; + } + expect NormalizeItem(out.Item.value) == NormalizeItem(record.item); + } + + method DecryptTests() { print "DecryptTests\n"; @@ -684,7 +751,7 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { { var exp := NormalizeItem(expected); for i := 0 to |actual| { - if actual[i] == exp { + if NormalizeItem(actual[i]) == exp { return true; } } @@ -780,6 +847,23 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { DDB.AttributeValue.N(nn.value) else value + case SS(s) => + var asSet := Seq.ToSet(s); + DDB.AttributeValue.SS(SortedSets.ComputeSetToOrderedSequence2(asSet, CharLess)) + case NS(s) => + var normList := Seq.MapWithResult(n => Norm.NormalizeNumber(n), s); + if normList.Success? then + var asSet := Seq.ToSet(normList.value); + DDB.AttributeValue.NS(SortedSets.ComputeSetToOrderedSequence2(asSet, CharLess)) + else + value + case BS(s) => + var asSet := Seq.ToSet(s); + DDB.AttributeValue.BS(SortedSets.ComputeSetToOrderedSequence2(asSet, ByteLess)) + case L(list) => + DDB.AttributeValue.L(Seq.Map(n => Normalize(n), list)) + case M(m) => + DDB.AttributeValue.M(map k <- m :: k := Normalize(m[k])) case _ => value } } @@ -961,7 +1045,7 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { function MakeEmptyTestVector() : TestVectorConfig { - TestVectorConfig(MakeCreateTableInput(), [], map[], [], map[], map[], [], [], [], [], [], [], []) + TestVectorConfig(MakeCreateTableInput(), [], map[], [], map[], map[], [], [], [], [], [], [], [], []) } method ParseTestVector(data : JSON, prev : TestVectorConfig) returns (output : Result) @@ -979,6 +1063,7 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { var gsi : seq := []; var tableEncryptionConfigs : map := map[]; var writeTests : seq := []; + var roundTripTests : seq := []; var decryptTests : seq := []; for i := 0 to |data.obj| { @@ -995,6 +1080,7 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { case "GSI" => gsi :- GetGSIs(data.obj[i].1); case "tableEncryptionConfigs" => tableEncryptionConfigs :- GetTableConfigs(data.obj[i].1); case "WriteTests" => writeTests :- GetWriteTests(data.obj[i].1); + case "RoundTripTest" => roundTripTests :- GetRoundTripTests(data.obj[i].1); case "DecryptTests" => decryptTests :- GetDecryptTests(data.obj[i].1); case _ => return Failure("Unexpected top level tag " + data.obj[i].0); } @@ -1015,11 +1101,29 @@ module {:options "-functionSyntax:4"} DdbEncryptionTestVectors { configsForIoTest := prev.configsForIoTest + ioPairs, configsForModTest := prev.configsForModTest + queryPairs, writeTests := prev.writeTests + writeTests, + roundTripTests := prev.roundTripTests + roundTripTests, decryptTests := prev.decryptTests + decryptTests ) ); } + method GetRoundTripTests(data : JSON) returns (output : Result, string>) + { + :- Need(data.Object?, "RoundTripTest Test must be an object."); + var configs : map := map[]; + var records : seq := []; + + for i := 0 to |data.obj| { + var obj := data.obj[i]; + match obj.0 { + case "Configs" => var src :- GetTableConfigs(obj.1); configs := src; + case "Records" => var src :- GetRecords(obj.1); records := src; + case _ => return Failure("Unexpected part of a write test : '" + obj.0 + "'"); + } + } + return Success([RoundTripTest(configs, records)]); + } + method GetWriteTests(data : JSON) returns (output : Result , string>) { :- Need(data.Array?, "Write Test list must be an array."); diff --git a/TestVectors/dafny/DDBEncryption/src/WriteSetPermutations.dfy b/TestVectors/dafny/DDBEncryption/src/WriteSetPermutations.dfy new file mode 100644 index 000000000..e3e3b313d --- /dev/null +++ b/TestVectors/dafny/DDBEncryption/src/WriteSetPermutations.dfy @@ -0,0 +1,142 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +include "../Model/AwsCryptographyDynamoDbEncryptionTypesWrapped.dfy" +include "CreateInterceptedDDBClient.dfy" +include "JsonItem.dfy" +include "Permute.dfy" + +module {:options "-functionSyntax:4"} WriteSetPermutations { + import opened JSON.Values + import BoundedInts + import JSON.API + import FileIO + import opened StandardLibrary.String + import opened Permutations + import Base64 + + type Bytes = seq + type BytesList = seq + type StringList = seq + + function GetConfigs() : JSON + { + Object([("AllSign", + Object([("attributeActionsOnEncrypt", + Object([ + ("RecNum", String("SIGN_ONLY")), + ("StringSet", String("SIGN_ONLY")), + ("NumberSet", String("SIGN_ONLY")), + ("BinarySet", String("SIGN_ONLY")) + ]) + )])), + ("AllEncrypt", + Object([("attributeActionsOnEncrypt", + Object([ + ("RecNum", String("SIGN_ONLY")), + ("StringSet", String("ENCRYPT_AND_SIGN")), + ("NumberSet", String("ENCRYPT_AND_SIGN")), + ("BinarySet", String("ENCRYPT_AND_SIGN")) + ]) + )]) + )]) + } + + function {:tailrecursion} GetStringArray(str : StringList, acc : seq := []) : JSON + { + if |str| == 0 then + Array(acc) + else + GetStringArray(str[1..], acc + [String(str[0])]) + } + + function {:tailrecursion} EncodeStrings(bytes : BytesList, acc : seq := []) : seq + { + if |bytes| == 0 then + acc + else + EncodeStrings(bytes[1..], acc + [Base64.Encode(bytes[0])]) + } + + function GetBinaryArray(bytes : BytesList) : JSON + { + var strs := EncodeStrings(bytes); + GetStringArray(strs) + } + + + function {:opaque} GetRecord(recNum : int, str : StringList, num : StringList, bytes : BytesList) : JSON + { + var numStr := Base10Int2String(recNum); + Object([ + ("RecNum", Object([("N", String(numStr))])), + ("StringSet", Object([("SS", GetStringArray(str))])), + ("NumberSet", Object([("NS", GetStringArray(num))])), + ("BinarySet", Object([("BS", GetBinaryArray(bytes))])) + ]) + } + + + function {:opaque} {:tailrecursion} GetRecords2( + recNum : int, + str : seq, + num : seq, + bytes : seq, + acc : seq := []) : seq + decreases str + decreases num + decreases bytes + { + if |str| == 0 || |num| == 0 || |bytes| == 0 then + acc + else + var newRec := GetRecord(recNum, str[0], num[0], bytes[0]); + GetRecords2(recNum+1, str[1..], num[1..], bytes[1..], acc + [newRec]) + } + + method GetRecords() returns (result : seq) + { + var recs : seq := []; + var recs1 := [GetRecord(200, ["aaa"], ["111"], [[1,2,3]])]; + + var p2s := GeneratePermutations(["aaa", "bbb"]); + var p2n := GeneratePermutations(["111", "222"]); + var p2b := GeneratePermutations([[1,2,3], [2,3,4]]); + var recs2 := GetRecords2(201, p2s, p2n, p2b); + + var p3s := GeneratePermutations(["aaa", "bbb", "ccc"]); + var p3n := GeneratePermutations(["111", "222", "333"]); + var p3b := GeneratePermutations([[1,2,3], [2,3,4], [3,4,5]]); + var recs3 := GetRecords2(203, p3s, p3n, p3b); + + var p4s := GeneratePermutations(["aaa", "bbb", "ccc", "ddd"]); + var p4n := GeneratePermutations(["111", "222", "333", "444"]); + var p4b := GeneratePermutations([[1,2,3], [2,3,4], [3,4,5], [4,5,6]]); + var recs4 := GetRecords2(209, p4s, p4n, p4b); + + return recs1 + recs2 + recs3 + recs4; + } + + function BytesBv(bits: seq): seq + { + seq(|bits|, i requires 0 <= i < |bits| => bits[i] as bv8) + } + + method WriteSetPermutations() + { + var configs := GetConfigs(); + var records := GetRecords(); + var whole := Object([("RoundTripTest", Object([ + ("Records", Array(records)), + ("Configs", configs) + ]))]); + + var jsonBytes :- expect API.Serialize(whole); + var jsonBv := BytesBv(jsonBytes); + + var _ :- expect FileIO.WriteBytesToFile( + "PermTest.json", + jsonBv + ); + } +} diff --git a/TestVectors/runtimes/java/build.gradle.kts b/TestVectors/runtimes/java/build.gradle.kts index a0222386a..e8dcefa67 100644 --- a/TestVectors/runtimes/java/build.gradle.kts +++ b/TestVectors/runtimes/java/build.gradle.kts @@ -1,3 +1,6 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + import java.net.URI import javax.annotation.Nullable import org.gradle.api.tasks.testing.logging.TestExceptionFormat @@ -51,8 +54,8 @@ repositories { name = "DynamoDB Local Release Repository - US West (Oregon) Region" url = URI.create("https://s3-us-west-2.amazonaws.com/dynamodb-local/release") } - mavenCentral() mavenLocal() + mavenCentral() if (caUrl != null && caPassword != null) { maven { name = "CodeArtifact" @@ -73,7 +76,7 @@ dependencies { implementation("org.dafny:DafnyRuntime:4.1.0") implementation("software.amazon.smithy.dafny:conversion:0.1") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.0") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:TestAwsCryptographicMaterialProviders:1.0-SNAPSHOT") implementation(platform("software.amazon.awssdk:bom:2.20.138")) diff --git a/TestVectors/runtimes/java/data.json b/TestVectors/runtimes/java/data.json index 4bbc334ad..24fe5fc60 100644 --- a/TestVectors/runtimes/java/data.json +++ b/TestVectors/runtimes/java/data.json @@ -1,4 +1,251 @@ { + "RoundTripTest": { + "Configs": { + "AllSign": { + "attributeActionsOnEncrypt": { + "RecNum": "SIGN_ONLY", + "String": "SIGN_ONLY", + "Number": "SIGN_ONLY", + "Bytes": "SIGN_ONLY", + "StringSet": "SIGN_ONLY", + "NumberSet": "SIGN_ONLY", + "BinarySet": "SIGN_ONLY", + "Map": "SIGN_ONLY", + "List": "SIGN_ONLY", + "Null": "SIGN_ONLY", + "Bool": "SIGN_ONLY" + } + }, + "AllNothing": { + "attributeActionsOnEncrypt": { + "RecNum": "SIGN_ONLY", + "String": "DO_NOTHING", + "Number": "DO_NOTHING", + "Bytes": "DO_NOTHING", + "StringSet": "DO_NOTHING", + "NumberSet": "DO_NOTHING", + "BinarySet": "DO_NOTHING", + "Map": "DO_NOTHING", + "List": "DO_NOTHING", + "Null": "DO_NOTHING", + "Bool": "DO_NOTHING" + }, + "allowedUnsignedAttributes": [ + "String", + "Number", + "Bytes", + "StringSet", + "NumberSet", + "BinarySet", + "Map", + "List", + "Null", + "Bool" + ] + }, + "AllEncrypt": { + "attributeActionsOnEncrypt": { + "RecNum": "SIGN_ONLY", + "String": "ENCRYPT_AND_SIGN", + "Number": "ENCRYPT_AND_SIGN", + "Bytes": "ENCRYPT_AND_SIGN", + "StringSet": "ENCRYPT_AND_SIGN", + "NumberSet": "ENCRYPT_AND_SIGN", + "BinarySet": "ENCRYPT_AND_SIGN", + "Map": "ENCRYPT_AND_SIGN", + "List": "ENCRYPT_AND_SIGN", + "Null": "ENCRYPT_AND_SIGN", + "Bool": "ENCRYPT_AND_SIGN" + } + } + }, + "Records": [ + { + "RecNum": { + "N": "100" + }, + "StringSet": { + "SS": [ + "aaa", + "bbb" + ] + }, + "NumberSet": { + "NS": [ + "1.0", + "2.0" + ] + }, + "BinarySet": { + "BS": [ + "b25l", + "dHdv" + ] + } + }, + { + "RecNum": { + "N": "101" + }, + "StringSet": { + "SS": [ + "bbb", + "aaa" + ] + }, + "NumberSet": { + "NS": [ + "2.0", + "1.0" + ] + }, + "BinarySet": { + "BS": [ + "dHdv", + "b25l" + ] + }, + "Bool": { + "BOOL": true + } + }, + { + "RecNum": { + "N": "102" + }, + "StringSet": { + "SS": [ + "ddd", + "bbb", + "aaa", + "ccc", + "eee" + ] + }, + "NumberSet": { + "NS": [ + "2.0", + "1.0", + "-000.000", + "+123.456", + "123.456e5", + ".99999999999999999999999999999999999999E+126", + "1234567890123456789012345678901234567800000000000000000000000000000" + ] + }, + "BinarySet": { + "BS": [ + "dHdv", + "b25l" + ] + }, + "Bool": { + "BOOL": true + }, + "Map": { + "M": { + "eee": { + "SS": [ + "ddd", + "bbb", + "aaa", + "ccc", + "eee" + ] + }, + "aaa": { + "NS": [ + "2.0", + "1.0", + "-000.000", + "+123.456", + "123.456e5", + ".99999999999999999999999999999999999999E+126", + "1234567890123456789012345678901234567800000000000000000000000000000" + ] + }, + "ccc": { + "BS": [ + "dHdv", + "b25l" + ] + } + } + }, + "List": { + "L": [ + { + "SS": [ + "ddd", + "bbb", + "aaa", + "ccc", + "eee" + ] + }, + { + "NS": [ + "2.0", + "1.0", + "-000.000", + "+123.456", + "123.456e5", + ".99999999999999999999999999999999999999E+126", + "1234567890123456789012345678901234567800000000000000000000000000000" + ] + }, + { + "BS": [ + "dHdv", + "b25l" + ] + } + ] + } + }, + { + "RecNum": { + "N": "103" + }, + "StringSet": { + "SS": [ + "aaa" + ] + }, + "NumberSet": { + "NS": [ + "1.0" + ] + }, + "BinarySet": { + "BS": [ + "b25l" + ] + }, + "String": { + "S": "" + }, + "Number": { + "N": "0" + }, + "Bytes": { + "B": "" + }, + "Map": { + "M": {} + }, + "List": { + "L": [] + }, + "Null": { + "NULL": "" + }, + "Bool": { + "BOOL": false + } + } + ] + }, "IoPairs": [ [ "1", @@ -31,7 +278,9 @@ ":six": "Seis", ":seven": "Siete", ":eight": "Ocho", - ":NumberTest" : {"N" : "0800.000e0"}, + ":NumberTest": { + "N": "0800.000e0" + }, ":nine": "Nueve", ":cmp1a": "F_Cuatro.S_Junk", ":cmp1b": "F_444.S_Junk", @@ -195,13 +444,15 @@ { "Query": "cmp1c < Comp1", "Fail": [ - 0,1 + 0, + 1 ] }, { "Query": "cmp1c = Comp1", "Fail": [ - 0,1 + 0, + 1 ] }, { diff --git a/codebuild/release/release-prod.yml b/codebuild/release/release-prod.yml index 6248a1430..dd6c989cd 100644 --- a/codebuild/release/release-prod.yml +++ b/codebuild/release/release-prod.yml @@ -4,8 +4,6 @@ version: 0.2 env: - variables: - BRANCH: "main" parameter-store: ACCOUNT: /CodeBuild/AccountId secrets-manager: @@ -31,7 +29,6 @@ phases: - cd aws-database-encryption-sdk-dynamodb-java/ pre_build: commands: - - git checkout $BRANCH - aws secretsmanager get-secret-value --region us-west-2 --secret-id Maven-GPG-Keys-Release --query SecretBinary --output text | base64 -d > ~/mvn_gpg.tgz - tar -xvf ~/mvn_gpg.tgz -C ~ # Create default location where GPG looks for creds and keys diff --git a/codebuild/release/release.yml b/codebuild/release/release.yml index 9111a41cc..149b84661 100644 --- a/codebuild/release/release.yml +++ b/codebuild/release/release.yml @@ -28,7 +28,7 @@ batch: - identifier: validate_staging_corretto11 depend-on: - - release_staging + - validate_staging_corretto8 buildspec: codebuild/staging/validate-staging.yml env: variables: @@ -38,7 +38,7 @@ batch: - identifier: validate_staging_corretto17 depend-on: - - release_staging + - validate_staging_corretto11 buildspec: codebuild/staging/validate-staging.yml env: variables: @@ -73,7 +73,7 @@ batch: - identifier: validate_release_corretto11 depend-on: - - upload_to_sonatype + - validate_release_corretto8 buildspec: codebuild/release/validate-release.yml env: variables: @@ -83,7 +83,7 @@ batch: - identifier: validate_release_corretto17 depend-on: - - upload_to_sonatype + - validate_release_corretto11 buildspec: codebuild/release/validate-release.yml env: variables: diff --git a/specification/dynamodb-encryption-client/ddb-attribute-serialization.md b/specification/dynamodb-encryption-client/ddb-attribute-serialization.md index 87c8b4307..a0be3ec80 100644 --- a/specification/dynamodb-encryption-client/ddb-attribute-serialization.md +++ b/specification/dynamodb-encryption-client/ddb-attribute-serialization.md @@ -60,9 +60,11 @@ String MUST be serialized as UTF-8 encoded bytes. #### Number -Number MUST be serialized as UTF-8 encoded bytes. Note that DynamoDB Number Attribute Values are strings. +This value MUST be normalized in the same way as DynamoDB normalizes numbers. +This normalized value MUST then be serialized as UTF-8 encoded bytes. + #### Binary Binary MUST be serialized with the identity function; @@ -107,8 +109,18 @@ Each of these entries MUST be serialized as: All [Set Entry Values](#set-entry-value) are the same type. Binary Sets MUST NOT contain duplicate entries. +Entries in a Binary Set MUST be ordered lexicographically by their underlying bytes in ascending order. + Number Sets MUST NOT contain duplicate entries. +Entries in a Number Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). +This ordering MUST be applied after normalization of the number value. +Note that because normalized number characters are all in the ASCII range (U+0000 to U+007F), +this ordering is equivalent to the [code point ordering](./string-ordering.md#code-point-order). + String Sets MUST NOT contain duplicate entries. +Entries in a String Set MUST be ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). +Note that though the entries are sorted by UTF016 binary order, +the values are serialized in the set with UTF-8 encoding. ###### Set Entry Length @@ -157,6 +169,11 @@ Each key-value pair MUST be serialized as: This sequence MUST NOT contain duplicate [Map Keys](#map-key). +Entries in a serialized Map MUST be ordered by key value, +ordered in ascending [UTF-16 binary order](./string-ordering.md#utf-16-binary-order). +Note that even though the values are sorted according to UTF-16 binary order, +string values are actually encoded within the map as UTF-8. + ###### Key Type Key Type MUST be the [Type ID](#type-id) for Strings. diff --git a/specification/dynamodb-encryption-client/ddb-encryption-branch-key-id-supplier.md b/specification/dynamodb-encryption-client/ddb-encryption-branch-key-id-supplier.md index 43462aa33..f9a48124e 100644 --- a/specification/dynamodb-encryption-client/ddb-encryption-branch-key-id-supplier.md +++ b/specification/dynamodb-encryption-client/ddb-encryption-branch-key-id-supplier.md @@ -1,3 +1,6 @@ +[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved." +[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0" + # DynamoDb Encryption Branch Key Supplier ## Overview diff --git a/specification/dynamodb-encryption-client/ddb-item-conversion.md b/specification/dynamodb-encryption-client/ddb-item-conversion.md index 76168263b..644624d58 100644 --- a/specification/dynamodb-encryption-client/ddb-item-conversion.md +++ b/specification/dynamodb-encryption-client/ddb-item-conversion.md @@ -26,10 +26,12 @@ This document describes how a DynamoDB Item is converted to the Structured Encryption Library's [Structured Data](../structured-encryption/structures.md#structured-data), and vice versa. -The conversion from DDB Item to Structured Data must be lossless, -meaning that converting a DDB Item to -a Structured Data and back to a DDB Item again -MUST result in the exact same DDB Item. +Round Trip conversion between DDB Item and Structured Data is technically lossless, but it is not identity. +The conversion normalizes some values, the same way that +DynamoDB PuItem followed by GetItem normalizes some values. +The sets still have the same members, and the numbers still have the same values, +but the members of the set might appear in a different order, +and the numeric value might be formatted differently. ## Convert DDB Item to Structured Data @@ -73,4 +75,4 @@ has the following requirements: - Conversion from a Structured Data Map MUST fail if it has duplicate keys - Conversion from a Structured Data Number Set MUST fail if it has duplicate values - Conversion from a Structured Data String Set MUST fail if it has duplicate values -- Conversion from a Structured Data Binary Set MUST fail if it has duplicate values \ No newline at end of file +- Conversion from a Structured Data Binary Set MUST fail if it has duplicate values diff --git a/specification/dynamodb-encryption-client/ddb-sdk-integration.md b/specification/dynamodb-encryption-client/ddb-sdk-integration.md index 0cb2716fb..3f48a692a 100644 --- a/specification/dynamodb-encryption-client/ddb-sdk-integration.md +++ b/specification/dynamodb-encryption-client/ddb-sdk-integration.md @@ -124,6 +124,9 @@ MUST have the following modified behavior: - [Encrypt before BatchWriteItem](#encrypt-before-batchwriteitem) - [Encrypt before TransactWriteItems](#encrypt-before-transactwriteitems) - [Decrypt after GetItem](#decrypt-after-getitem) +- [Decrypt after PutItem](#decrypt-after-putitem) +- [Decrypt after UpdateItem](#decrypt-after-updateitem) +- [Decrypt after DeleteItem](#decrypt-after-deleteitem) - [Decrypt after BatchGetItem](#decrypt-after-batchgetitem) - [Decrypt after Scan](#decrypt-after-scan) - [Decrypt after Query](#decrypt-after-query) @@ -334,6 +337,92 @@ Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. The GetItem response's `Item` field MUST be replaced by the encrypted DynamoDb Item outputted above. +### Decrypt after PutItem + +After a [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the PutItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-response-Attributes). + The response will contain Attributes if the related PutItem request's + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues) + had a value of `ALL_OLD` and the PutItem call replaced a pre-existing item. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The PutItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +### Decrypt after DeleteItem + +After a [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the DeleteItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-response-Attributes). + The response will contain Attributes if the related DeleteItem request's + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues) + had a value of `ALL_OLD` and an item was deleted. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The DeleteItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +### Decrypt after UpdateItem + +After a [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the UpdateItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-response-Attributes). +- the original UpdateItem request had a + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues) + with a value of `ALL_OLD` or `ALL_NEW`. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The UpdateItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +In all other cases, the UpdateItem response MUST NOT be modified. + +Additionally, if a value of `UPDATED_OLD` or `UPDATED_NEW` was used, +and any Attributes in the response are authenticated +per the [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration), +an error MUST be raised. +Given that we [validate UpdateItem requests](#validate-before-updateitem), +and thus updates will not modify any signed field, +an error here would indicate a bug in +our library or a bug within DynamoDB. + ### Decrypt after BatchGetItem After a [BatchGetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html) diff --git a/specification/dynamodb-encryption-client/ddb-support.md b/specification/dynamodb-encryption-client/ddb-support.md index 7697759e7..b276942f4 100644 --- a/specification/dynamodb-encryption-client/ddb-support.md +++ b/specification/dynamodb-encryption-client/ddb-support.md @@ -1,3 +1,6 @@ +[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved." +[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0" + # DynamoDB Support Layer The DynamoDB Support Layer provides everything necessary to the middleware interceptors, diff --git a/specification/dynamodb-encryption-client/string-ordering.md b/specification/dynamodb-encryption-client/string-ordering.md new file mode 100644 index 000000000..7b2cb8bc8 --- /dev/null +++ b/specification/dynamodb-encryption-client/string-ordering.md @@ -0,0 +1,103 @@ +[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved." +[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0" + +# String Ordering + +## Version + +1.0.0 + +### Changelog + +- 1.0.0 + + - Initial record + +## Definitions + +### Conventions used in this document + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" +in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +### Unicode + +For the latest version see: +https://www.unicode.org/versions/latest/ + +For Version 15.0 see: +https://www.unicode.org/versions/Unicode15.0.0/ + +### Unicode scalar value + +Any Unicode code point except [surrogate](#surrogates) code points. + +See [section 3.9 Unicode Encoding Forms](https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf) of the Unicode specification. + +### UTF-16 code unit + +A 16-bit value representing a Unicode code point in a UTF-16 encoding. +Includes [surrogate](#surrogates) code points. + +See [section 3.9 Unicode Encoding Forms](https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf) of the Unicode specification. + +### Surrogates + +Unicode code points in the range U+D800 to U+DFFF. +These code points are only used in UTF-16 encodings +to represent Unicode values above U+FFFF. + +See [section 3.8 Surrogates](https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf) of the Unicode specification. + +## Overview + +There are several instances throughout this specification where an order must be +imposed on an unordered data structure during serialization for the purposes of canonicalization. +This means that we must clearly specify the canonical ordering wherever +such serialization is required. +This is especially importing when ordering strings, +as different encodings may lend themselves to slightly different orderings. + +Wherever strings need to be ordered, +this specification will require either a [code point order](#code-point-order) +or a [UTF-16 binary order](#utf-16-binary-order). + +## UTF-16 Binary Order + +When ordering strings, +these strings MUST be compared according to their UTF-16 encoding, +lexicographically per [UTF-16 code unit](#utf-16-code-unit). +UTF-16 code units for [high or low surrogates](#surrogates) MUST be compared individually, +and the [Unicode scalar value](#unicode-scalar-value) represented by a surrogate pair +MUST NOT be compared. + +Note that this is not equivalent to the [code point order](#code-point-order). +Specifically, the range of characters with Unicode code point U+E000 to U+0xFFFF +(code points representable by 16 bits, but after the surrogate range) +MUST be considered "greater than" any character with a Unicode code point of U+10000 to U+10FFFF. + +As an example, consider the following two characters: + +| char | Unicode code point | UTF-16 encoding | +| ---- | ------------------ | --------------- | +| `。` | U+FF61 | 0xFF61 | +| `𐀂` | U+10002 | 0xD800 0xDC02 | + +This ordering will order `。` _after_ `𐀂`, despite `𐀂` having a higher Unicode code point. + +## Code Point Order + +This is the ordering referred to in the Unicode specification as a [code point order](https://www.unicode.org/versions/Unicode15.0.0/ch05.pdf). + +When ordering strings, +these strings are compared lexicographically per [Unicode scalar value](#unicode-scalar-value) represented by the string. +This means that if a string is UTF-16 encoded, +higher order Unicode characters, encoded as a surrogate pair, +must be handled as the Unicode scalar value represented by that surrogate pair, +instead of each surrogate code point being handled individually. + +Note that this is equivalent to lexicographically comparing a UTF-8 encoded string per byte. +This is also equivalent to lexicographically comparing a UTF-32 encoded string per 32-bit code unit. + +Currently, this specification does not directly use code point order for sorting string values, +but may use this ordering for new behaviors in the future. diff --git a/specification/structured-encryption/footer.md b/specification/structured-encryption/footer.md index 560259c42..b3dfc0444 100644 --- a/specification/structured-encryption/footer.md +++ b/specification/structured-encryption/footer.md @@ -1,3 +1,5 @@ +[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved." +[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0" # Structured Encryption Footer diff --git a/specification/structured-encryption/header.md b/specification/structured-encryption/header.md index 5d8ec6212..ec0593ff5 100644 --- a/specification/structured-encryption/header.md +++ b/specification/structured-encryption/header.md @@ -1,3 +1,5 @@ +[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved." +[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0" # Structured Encryption Header