From 403dfac9c235211d2feeaed62eb1774d14c58365 Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:31:35 -0500 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20issue=20when=20a=20DynamoDB=20Set=20?= =?UTF-8?q?attribute=20is=20marked=20as=20SIGN=5FONLY=20in=20th=E2=80=A6?= =?UTF-8?q?=20(#560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .github/workflows/ci_permute_java.yml | 85 ++ CHANGELOG.md | 13 + DecryptWithPermute/.gitignore | 5 + DecryptWithPermute/Makefile | 53 + DecryptWithPermute/README.md | 170 +++ ...DbEncryptionSdkDecryptWithPermuteTypes.dfy | 226 ++++ .../Model/DynamoDbPermuteDecryptor.smithy | 81 ++ .../src/AltDecryptStructure.dfy | 360 ++++++ .../src/AltDynamoToStruct.dfy | 1141 +++++++++++++++++ ...ryptionSdkDecryptWithPermuteOperations.dfy | 166 +++ .../dafny/DecryptWithPermute/src/Counter.dfy | 123 ++ .../dafny/DecryptWithPermute/src/Index.dfy | 65 + .../dafny/DecryptWithPermute/src/Permute.dfy | 107 ++ .../src/ValidatePermuted.dfy | 345 +++++ .../DecryptWithPermute/test/MakeSets.not_dfy | 488 +++++++ .../DecryptWithPermute/test/Validate.dfy | 244 ++++ .../runtimes/java/build.gradle.kts | 268 ++++ .../runtimes/java/gradle.properties | 1 + .../java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + DecryptWithPermute/runtimes/java/gradlew | 244 ++++ DecryptWithPermute/runtimes/java/gradlew.bat | 92 ++ .../runtimes/java/makefile_helper.sh | 21 + .../runtimes/java/settings.gradle.kts | 1 + .../internaldafny/__default.java | 4 + .../DynamoDbPermuteDecryptor.java | 93 ++ .../decryptwithpermute/ToDafny.java | 86 ++ .../decryptwithpermute/ToNative.java | 97 ++ .../model/CollectionOfErrors.java | 139 ++ .../model/DynamoDbPermuteDecryptorConfig.java | 77 ++ .../DynamoDbPermuteDecryptorException.java | 107 ++ .../decryptwithpermute/model/OpaqueError.java | 142 ++ .../model/PermuteDecryptInput.java | 103 ++ .../model/PermuteDecryptOutput.java | 103 ++ .../DynamoDbEncryption/src/ConfigToInfo.dfy | 2 + .../DynamoDbEncryption/src/DDBSupport.dfy | 2 + .../DynamoDbEncryption/src/DynamoToStruct.dfy | 60 +- .../DynamoDbEncryption/src/SearchInfo.dfy | 15 +- .../test/DynamoToStruct.dfy | 264 +++- ...tionSdkDynamoDbItemEncryptorOperations.dfy | 1 + .../dafny/DynamoDbItemEncryptor/src/Index.dfy | 1 + .../runtimes/java/build.gradle.kts | 2 +- .../java/DynamoDbEncryption/build.gradle.kts | 2 +- .../Migration/DDBECToAWSDBE/build.gradle.kts | 2 +- .../PlaintextToAWSDBE/build.gradle.kts | 2 +- README.md | 4 +- TestVectors/dafny/DDBEncryption/src/Index.dfy | 4 + .../dafny/DDBEncryption/src/Permute.dfy | 72 ++ .../dafny/DDBEncryption/src/TestVectors.dfy | 108 +- .../src/WriteSetPermutations.dfy | 142 ++ TestVectors/runtimes/java/build.gradle.kts | 7 +- TestVectors/runtimes/java/data.json | 257 +++- .../ddb-attribute-serialization.md | 19 +- .../ddb-encryption-branch-key-id-supplier.md | 3 + .../ddb-item-conversion.md | 12 +- .../dynamodb-encryption-client/ddb-support.md | 3 + .../string-ordering.md | 103 ++ specification/structured-encryption/footer.md | 2 + specification/structured-encryption/header.md | 2 + 59 files changed, 6290 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/ci_permute_java.yml create mode 100644 DecryptWithPermute/.gitignore create mode 100644 DecryptWithPermute/Makefile create mode 100644 DecryptWithPermute/README.md create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/Model/AwsCryptographyDbEncryptionSdkDecryptWithPermuteTypes.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/Model/DynamoDbPermuteDecryptor.smithy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/AltDecryptStructure.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/AltDynamoToStruct.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/AwsCryptographyDbEncryptionSdkDecryptWithPermuteOperations.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/Counter.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/Index.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/Permute.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/src/ValidatePermuted.dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/test/MakeSets.not_dfy create mode 100644 DecryptWithPermute/dafny/DecryptWithPermute/test/Validate.dfy create mode 100644 DecryptWithPermute/runtimes/java/build.gradle.kts create mode 100644 DecryptWithPermute/runtimes/java/gradle.properties create mode 100644 DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.jar create mode 100644 DecryptWithPermute/runtimes/java/gradle/wrapper/gradle-wrapper.properties create mode 100755 DecryptWithPermute/runtimes/java/gradlew create mode 100644 DecryptWithPermute/runtimes/java/gradlew.bat create mode 100755 DecryptWithPermute/runtimes/java/makefile_helper.sh create mode 100644 DecryptWithPermute/runtimes/java/settings.gradle.kts create mode 100644 DecryptWithPermute/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/internaldafny/__default.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/DynamoDbPermuteDecryptor.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToDafny.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/ToNative.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/CollectionOfErrors.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorConfig.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/DynamoDbPermuteDecryptorException.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/OpaqueError.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptInput.java create mode 100644 DecryptWithPermute/runtimes/java/src/main/smithy-generated/software/amazon/cryptography/dbencryptionsdk/decryptwithpermute/model/PermuteDecryptOutput.java create mode 100644 TestVectors/dafny/DDBEncryption/src/Permute.dfy create mode 100644 TestVectors/dafny/DDBEncryption/src/WriteSetPermutations.dfy create mode 100644 specification/dynamodb-encryption-client/string-ordering.md 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..e993f2e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 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..9163bc53f --- /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.1") + 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 0000000000000000000000000000000000000000..943f0cbfa754578e88a3dae77fce6e3dea56edbf GIT binary patch literal 61574 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 ScNgau(U?ep-K_E5zy1%ZQTdPn literal 0 HcmV?d00001 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/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..92bcb3812 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.1" description = "Aws Database Encryption Sdk for DynamoDb Java" java { diff --git a/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts b/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts index 8c69ee14a..d2a870df9 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.1") 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/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index 8c374d8eb..dba71bfa3 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.1") 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..c98b992b0 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.1") 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..d5e379c77 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.1") 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/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-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 From 5401b048b809fe9557092aeb94807f45e03f322a Mon Sep 17 00:00:00 2001 From: Andrew Jewell <107044381+ajewellamz@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:05:01 -0500 Subject: [PATCH 2/3] chore: Examples run serially. Examples wait for GSI. No extra checkout (#561) --- ...oundBeaconSearchableEncryptionExample.java | 33 +++++++++++++------ ...tualBeaconSearchableEncryptionExample.java | 33 +++++++++++++------ codebuild/release/release-prod.yml | 3 -- codebuild/release/release.yml | 8 ++--- 4 files changed, 50 insertions(+), 27 deletions(-) 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/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: From ea42115d2f40f46a4f35ef03563e12d26167f132 Mon Sep 17 00:00:00 2001 From: lavaleri <49660121+lavaleri@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:48:57 -0800 Subject: [PATCH 3/3] fix: Decrypt attributes returned by all DDB APIs (#578) --- CHANGELOG.md | 8 + .../runtimes/java/build.gradle.kts | 2 +- .../src/DeleteItemTransform.dfy | 75 +++- .../src/DynamoDbMiddlewareSupport.dfy | 9 + .../src/PutItemTransform.dfy | 75 +++- .../src/UpdateItemTransform.dfy | 138 ++++++- .../runtimes/java/build.gradle.kts | 2 +- .../DynamoDbEncryptionInterceptor.java | 11 + .../datamodeling/encryption/DoNotEncrypt.java | 4 + .../datamodeling/encryption/DoNotTouch.java | 4 + ...EncryptionInterceptorIntegrationTests.java | 155 ++++++- ...ryptionEnhancedClientIntegrationTests.java | 179 +++++--- .../validdatamodels/AllTypesClass.java | 386 ++++++++++++++++++ .../java/DynamoDbEncryption/build.gradle.kts | 2 +- .../Migration/DDBECToAWSDBE/build.gradle.kts | 2 +- .../PlaintextToAWSDBE/build.gradle.kts | 2 +- TestVectors/runtimes/java/build.gradle.kts | 2 +- .../ddb-sdk-integration.md | 89 ++++ 18 files changed, 1082 insertions(+), 63 deletions(-) create mode 100644 DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e993f2e15..ecda8d9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 diff --git a/DecryptWithPermute/runtimes/java/build.gradle.kts b/DecryptWithPermute/runtimes/java/build.gradle.kts index 9163bc53f..d5e464531 100644 --- a/DecryptWithPermute/runtimes/java/build.gradle.kts +++ b/DecryptWithPermute/runtimes/java/build.gradle.kts @@ -68,7 +68,7 @@ repositories { val dynamodb by configurations.creating dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + 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") 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/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 92bcb3812..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.1" +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 d2a870df9..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.1") + 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/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index dba71bfa3..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.1") + 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 c98b992b0..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.1") + 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/TestVectors/runtimes/java/build.gradle.kts b/TestVectors/runtimes/java/build.gradle.kts index d5e379c77..e8dcefa67 100644 --- a/TestVectors/runtimes/java/build.gradle.kts +++ b/TestVectors/runtimes/java/build.gradle.kts @@ -76,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.1") + 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/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)