From 05464c17ffb0a60784c5944ceddfb80010c9dace Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:21:03 -0600 Subject: [PATCH 01/19] chore: use HTTPS for spec --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index b4e128ffa..b2d1f5495 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "specification"] path = specification - url = git@github.com:awslabs/aws-encryption-sdk-specification.git \ No newline at end of file + url = https://github.com/awslabs/aws-encryption-sdk-specification.git From cdcd3937fe26f48f984f3669bbe590721c2d6df4 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:29:03 -0600 Subject: [PATCH 02/19] remove spec --- .gitmodules | 3 --- specification | 1 - 2 files changed, 4 deletions(-) delete mode 160000 specification diff --git a/.gitmodules b/.gitmodules index b2d1f5495..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "specification"] - path = specification - url = https://github.com/awslabs/aws-encryption-sdk-specification.git diff --git a/specification b/specification deleted file mode 160000 index 280a89401..000000000 --- a/specification +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 280a894019cd1b4efc6b16cfb233bf1ec21bc508 From d5e5a3cfa3faf801f90c21a55d0c4f77b55ad109 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:58:25 -0600 Subject: [PATCH 03/19] duvet: point specification to private --- .gitmodules | 4 ++++ specification | 1 + 2 files changed, 5 insertions(+) create mode 160000 specification diff --git a/.gitmodules b/.gitmodules index e69de29bb..dfb50261f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "private_aws"] + path = specification + url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git + branch = tonyknap/todo-cbc-encryption diff --git a/specification b/specification new file mode 160000 index 000000000..87f974b22 --- /dev/null +++ b/specification @@ -0,0 +1 @@ +Subproject commit 87f974b22cbc1678f6be3dfd821c89c0fd51a595 From 5a1c06c3989a281cb27172676f70b427cc345691 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:45:47 -0600 Subject: [PATCH 04/19] chore(duvet): modernize duvet --- .duvet/.gitignore | 3 +++ .duvet/config.toml | 21 ++++++++++++++++++ .github/workflows/duvet.yml | 44 +++++++++++++++++++++++++++++++++++++ Makefile | 19 +++++++--------- specification | 2 +- 5 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 .duvet/.gitignore create mode 100644 .duvet/config.toml create mode 100644 .github/workflows/duvet.yml diff --git a/.duvet/.gitignore b/.duvet/.gitignore new file mode 100644 index 000000000..93956e36d --- /dev/null +++ b/.duvet/.gitignore @@ -0,0 +1,3 @@ +reports/ +requirements/ +specification/ \ No newline at end of file diff --git a/.duvet/config.toml b/.duvet/config.toml new file mode 100644 index 000000000..9c8d5692f --- /dev/null +++ b/.duvet/config.toml @@ -0,0 +1,21 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "src/**/*.java" + +# Include required specifications here +[[specification]] +source = "specification/s3-encryption/client.md" +[[specification]] +source = "specification/s3-encryption/materials/keyrings.md" +[[specification]] +source = "specification/s3-encryption/materials/s3-keyring.md" +[[specification]] +source = "specification/s3-encryption/materials/s3-kms-keyring.md" + +[report.html] +enabled = true + +# Enable snapshots to prevent requirement coverage regressions +[report.snapshot] +enabled = false diff --git a/.github/workflows/duvet.yml b/.github/workflows/duvet.yml new file mode 100644 index 000000000..366348689 --- /dev/null +++ b/.github/workflows/duvet.yml @@ -0,0 +1,44 @@ +name: duvet + +on: + workflow_call: + # Optional inputs that can be provided when calling this workflow + +jobs: + test: + runs-on: macos-latest + permissions: + id-token: write + contents: read + pages: write + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + submodules: true + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Clone duvet repository + run: git clone https://github.com/awslabs/duvet.git /tmp/duvet + + - name: Build and install duvet + run: | + cd /tmp/duvet + cargo xtask build + cargo install --path ./duvet + + - name: Run duvet + run: make duvet + + - name: Upload duvet reports + uses: actions/upload-artifact@v4 + with: + name: reports + include-hidden-files: true + path: .duvet/reports/report.html + diff --git a/Makefile b/Makefile index 1c60b7a64..1288d27da 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,12 @@ # Used for misc supporting functions like Duvet and prettier. Builds, tests, etc. should use the usual Java/Maven tooling. -duvet: | duvet_extract duvet_report - -duvet_extract: - rm -rf compliance - $(foreach file, $(shell find specification/s3-encryption -name '*.md'), duvet extract -o compliance -f MARKDOWN $(file);) +duvet: | duvet_clean duvet_report duvet_report: - duvet \ - report \ - --spec-pattern "compliance/**/*.toml" \ - --source-pattern "src/**/*.java" \ - --source-pattern "compliance_exceptions/*.txt" \ - --html specification_compliance_report.html + duvet report + +duvet-view-report-mac: + open .duvet/reports/report.html + +duvet_clean: + rm -rf .duvet/reports/ .duvet/requirements/ diff --git a/specification b/specification index 87f974b22..280a89401 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 87f974b22cbc1678f6be3dfd821c89c0fd51a595 +Subproject commit 280a894019cd1b4efc6b16cfb233bf1ec21bc508 From afd8c6508465f2605b749f755c7c3be064b2e060 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:13:05 -0600 Subject: [PATCH 05/19] feat(spec): bump spec to v4.0.1 canidate --- .gitmodules | 2 +- specification | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index dfb50261f..9dd945251 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "private_aws"] path = specification url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git - branch = tonyknap/todo-cbc-encryption + branch = tonyknap/v4.0.1-candidate diff --git a/specification b/specification index 280a89401..ee5d97ae1 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 280a894019cd1b4efc6b16cfb233bf1ec21bc508 +Subproject commit ee5d97ae109395752273501373f9ca800bcf3bcf From cf8ea4fa946cfaba816b31e254a5e97dae7f8ba1 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:28:50 -0600 Subject: [PATCH 06/19] fix(duvet): Remove or comment out broken duvetxsx --- .../ContentMetadataDecodingStrategy.java | 33 +++++++++---------- .../s3/internal/MetadataKeyConstants.java | 12 +++---- .../internal/ContentMetadataStrategyTest.java | 27 ++------------- 3 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index baa819aae..be4fa001e 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -303,8 +303,7 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get break; case ALG_AES_256_GCM_IV12_TAG16_NO_KDF: case ALG_AES_256_CTR_IV16_TAG16_NO_KDF: - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //# - The mapkey "x-amz-tag-len" MUST be present for V2 format objects. + final int tagLength = Integer.parseInt(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH)); if (tagLength != algorithmSuite.cipherTagLengthBits()) { throw new S3EncryptionClientException("Expected tag length (bits) of: " @@ -423,10 +422,10 @@ public Map loadInstructionFileMetadata(GetObjectRequest request) * All V1/V2 keys must be present in object metadata. */ public static boolean isV1V2InObjectMetadata(Map objectMetadata) { - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. return objectMetadata.containsKey(MetadataKeyConstants.CONTENT_IV) && (objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) || objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2)); @@ -437,8 +436,8 @@ public static boolean isV1V2InObjectMetadata(Map objectMetadata) * "x-amz-c" and "x-amz-d" and "x-amz-i" keys are always in object metadata, and "x-amz-3" is also in object metadata. */ public static boolean isV3InObjectMetadata(Map objectMetadata) { - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. return objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3) && objectMetadata.containsKey(MetadataKeyConstants.KEY_COMMITMENT_V3) && objectMetadata.containsKey(MetadataKeyConstants.MESSAGE_ID_V3) @@ -512,23 +511,23 @@ public ContentMetadata decodeV3FromInstructionFile(GetObjectRequest request, Get public ContentMetadata decode(GetObjectRequest request, GetObjectResponse response) { Map objectMetadata = response.metadata(); - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=exception - //# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///= type=exception + ///# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. if (objectMetadata != null) { // V1/V2 in Object Metadata - All V1/V2 keys present in object metadata - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. + ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ///# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. if (isV1V2InObjectMetadata(objectMetadata)) { return readFromMapV1V2(objectMetadata, response); } // V3 in Object Metadata - c/d/i always in object metadata, x-amz-3 also in object metadata - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. + ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ////# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata. else if (isV3InObjectMetadata(objectMetadata)) { diff --git a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java index 8f3bd0a51..319d0934d 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java @@ -71,8 +71,8 @@ public class MetadataKeyConstants { public static boolean isV1Format(Map metadata) { return metadata.containsKey(CONTENT_IV) && metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ////# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); @@ -85,8 +85,8 @@ public static boolean isV2Format(Map metadata) { // TODO-Post-Pentest: Objects copied without x-amz-matdesc was able be decrypted by V2 Client. // Should this mapkey be SHOULD instead of MUST? // metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ////# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); @@ -98,8 +98,8 @@ public static boolean isV3Format(Map metadata) { metadata.containsKey(ENCRYPTED_DATA_KEY_ALGORITHM_V3) && metadata.containsKey(KEY_COMMITMENT_V3) && metadata.containsKey(MESSAGE_ID_V3) && - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + ////=// specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + ////#// If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V3) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V1); diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java index eea456a09..3020f5570 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java @@ -63,9 +63,6 @@ public void setUp() { @Test public void testDetectV1Format() { - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. Map metadata = new HashMap<>(); metadata.put("x-amz-iv", "dGVzdC1pdi0xMi1i"); // base64 of "test-iv-12-b" metadata.put("x-amz-key", "ZW5jcnlwdGVkLWtleS1kYXRh"); // base64 of "encrypted-key-data" @@ -447,9 +444,6 @@ public void testExclusiveKeysCollision() { .metadata(metadata) .build(); - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> decodingStrategy.decode(getObjectRequest, response)); assertTrue(exception.getMessage().contains("Content metadata is tampered, required metadata to decrypt the object are missing")); } @@ -515,9 +509,7 @@ static Stream provideMetadataFormatDetection() { //= type=test //# - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects. v2Metadata.put("x-amz-cek-alg", "AES/GCM/NoPadding"); - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //= type=test - //# - The mapkey "x-amz-tag-len" MUST be present for V2 format objects. + v2Metadata.put("x-amz-tag-len", "128"); Map v2CbcMetadata = new HashMap<>(); @@ -541,9 +533,7 @@ static Stream provideMetadataFormatDetection() { //= type=test //# - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects. v2CbcMetadata.put("x-amz-cek-alg", "AES/CBC/PKCS5Padding"); - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //= type=test - //# - The mapkey "x-amz-tag-len" MUST be present for V2 format objects. + v2CbcMetadata.put("x-amz-tag-len", "128"); @@ -573,30 +563,19 @@ static Stream provideMetadataFormatDetection() { //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //= type=test //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. + Arguments.of(v1Metadata, "V1", AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF), //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //= type=test //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. Arguments.of(v2CbcMetadata, "V2", AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF), //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //= type=test //# Objects encrypted with ALG_AES_256_GCM_IV12_TAG16_NO_KDF MUST use the V2 message format version only. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. Arguments.of(v2Metadata, "V2", AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF), //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //= type=test //# Objects encrypted with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY MUST use the V3 message format version only. - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //= type=test - //# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. Arguments.of(v3Metadata, "V3", AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) ); } From 01421447cd7f45be640f865f91d93b4319ac147a Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:06:18 -0600 Subject: [PATCH 07/19] chore(duvet): update duvet config --- .duvet/config.toml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.duvet/config.toml b/.duvet/config.toml index 9c8d5692f..e11fed05a 100644 --- a/.duvet/config.toml +++ b/.duvet/config.toml @@ -7,11 +7,17 @@ pattern = "src/**/*.java" [[specification]] source = "specification/s3-encryption/client.md" [[specification]] -source = "specification/s3-encryption/materials/keyrings.md" +source = "specification/s3-encryption/decryption.md" [[specification]] -source = "specification/s3-encryption/materials/s3-keyring.md" +source = "specification/s3-encryption/encryption.md" [[specification]] -source = "specification/s3-encryption/materials/s3-kms-keyring.md" +source = "specification/s3-encryption/key-commitment.md" +[[specification]] +source = "specification/s3-encryption/key-derivation.md" +[[specification]] +source = "specification/s3-encryption/data-format/content-metadata.md" +[[specification]] +source = "specification/s3-encryption/data-format/metadata-strategy.md" [report.html] enabled = true From 8e812f459ef30905166752298c9a7e92c986f63c Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:05:01 -0600 Subject: [PATCH 08/19] fix(Duvet): Refactor metadata version detection and add exclusive key collision check - Split isV1V2InObjectMetadata() into separate isV1InObjectMetadata() and isV2InObjectMetadata() methods that properly check for version-exclusive keys - Add hasExclusiveKeyCollision() to detect when multiple version-exclusive keys (x-amz-key, x-amz-key-v2, x-amz-3) are present in metadata - Update decode() to throw exception when exclusive key collision is detected (requirement 106) - Fix Duvet annotations for requirements 102, 103, 104, and 106 - Update tests to use new version detection methods --- .../ContentMetadataDecodingStrategy.java | 58 +++++++++++-------- .../s3/internal/MetadataKeyConstants.java | 18 ++++-- ...3EncryptionClientCommitmentPolicyTest.java | 6 +- .../internal/ContentMetadataStrategyTest.java | 2 +- 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index be4fa001e..04f0c16ab 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -227,9 +227,6 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //= type=exception //# - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V1 format objects. - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - - //# - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects. if (contentEncryptionAlgorithm == null || contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.cipherName())) { algorithmSuite = AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF; @@ -418,17 +415,27 @@ public Map loadInstructionFileMetadata(GetObjectRequest request) } /** - * Determines if V1/V2 format is present in object metadata. - * All V1/V2 keys must be present in object metadata. + * Determines if V1 format is present in object metadata. */ - public static boolean isV1V2InObjectMetadata(Map objectMetadata) { - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. + public static boolean isV1InObjectMetadata(Map objectMetadata) { + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-iv" and "x-amz-key" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V1 format. return objectMetadata.containsKey(MetadataKeyConstants.CONTENT_IV) - && (objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) - || objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2)); + && objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2) + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3); + } + + /** + * Determines if V2 format is present in object metadata. + */ + public static boolean isV2InObjectMetadata(Map objectMetadata) { + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-iv" and "x-amz-key-v2" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V2 format. + return objectMetadata.containsKey(MetadataKeyConstants.CONTENT_IV) + && objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2) + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3); } /** @@ -436,8 +443,8 @@ public static boolean isV1V2InObjectMetadata(Map objectMetadata) * "x-amz-c" and "x-amz-d" and "x-amz-i" keys are always in object metadata, and "x-amz-3" is also in object metadata. */ public static boolean isV3InObjectMetadata(Map objectMetadata) { - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" but no other version exclusive keys then the object MUST be considered an S3EC-encrypted object using the V3 format. return objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3) && objectMetadata.containsKey(MetadataKeyConstants.KEY_COMMITMENT_V3) && objectMetadata.containsKey(MetadataKeyConstants.MESSAGE_ID_V3) @@ -511,23 +518,26 @@ public ContentMetadata decodeV3FromInstructionFile(GetObjectRequest request, Get public ContentMetadata decode(GetObjectRequest request, GetObjectResponse response) { Map objectMetadata = response.metadata(); - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///= type=exception - ///# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# If there are multiple mapkeys which are meant to be exclusive to different versions, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. if (objectMetadata != null) { + if (MetadataKeyConstants.hasExclusiveKeyCollision(objectMetadata)) { + throw new S3EncryptionClientException("Content metadata is tampered, required metadata to decrypt the object are missing"); + } + // V1/V2 in Object Metadata - All V1/V2 keys present in object metadata - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///# - If the metadata contains "x-amz-iv" and "x-amz-key" then the object MUST be considered as an S3EC-encrypted object using the V1 format. - ///= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ///# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format. - if (isV1V2InObjectMetadata(objectMetadata)) { + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-iv" and "x-amz-key" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V1 format. + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-iv" and "x-amz-key-v2" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V2 format. + if (isV1InObjectMetadata(objectMetadata) || isV2InObjectMetadata(objectMetadata)) { return readFromMapV1V2(objectMetadata, response); } // V3 in Object Metadata - c/d/i always in object metadata, x-amz-3 also in object metadata - ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ////# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format. + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" but no other version exclusive keys then the object MUST be considered an S3EC-encrypted object using the V3 format. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata. else if (isV3InObjectMetadata(objectMetadata)) { diff --git a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java index 319d0934d..61b30feb6 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java @@ -71,8 +71,6 @@ public class MetadataKeyConstants { public static boolean isV1Format(Map metadata) { return metadata.containsKey(CONTENT_IV) && metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && - ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ////# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); @@ -85,8 +83,6 @@ public static boolean isV2Format(Map metadata) { // TODO-Post-Pentest: Objects copied without x-amz-matdesc was able be decrypted by V2 Client. // Should this mapkey be SHOULD instead of MUST? // metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && - ////= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ////# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); @@ -98,14 +94,24 @@ public static boolean isV3Format(Map metadata) { metadata.containsKey(ENCRYPTED_DATA_KEY_ALGORITHM_V3) && metadata.containsKey(KEY_COMMITMENT_V3) && metadata.containsKey(MESSAGE_ID_V3) && - ////=// specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - ////#// If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. metadata.containsKey(ENCRYPTED_DATA_KEY_V3) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V1); } + /** + * Checks if multiple version-exclusive keys are present in metadata. + */ + public static boolean hasExclusiveKeyCollision(Map metadata) { + boolean hasV1Key = metadata.containsKey(ENCRYPTED_DATA_KEY_V1); + boolean hasV2Key = metadata.containsKey(ENCRYPTED_DATA_KEY_V2); + boolean hasV3Key = metadata.containsKey(ENCRYPTED_DATA_KEY_V3); + + int exclusiveKeyCount = (hasV1Key ? 1 : 0) + (hasV2Key ? 1 : 0) + (hasV3Key ? 1 : 0); + return exclusiveKeyCount > 1; + } + /** * Compresses a wrapping algorithm name to its V3 format. */ diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCommitmentPolicyTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCommitmentPolicyTest.java index cfcaf452c..63e441f71 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCommitmentPolicyTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientCommitmentPolicyTest.java @@ -177,7 +177,7 @@ public void testCommitmentPolicyForbidEncryptAllowDecrypt() { //= specification/s3-encryption/key-commitment.md#commitment-policy //= type=test //# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST NOT encrypt using an algorithm suite which supports key commitment. - assertTrue(ContentMetadataDecodingStrategy.isV1V2InObjectMetadata(metadata)); + assertTrue(ContentMetadataDecodingStrategy.isV1InObjectMetadata(metadata) || ContentMetadataDecodingStrategy.isV2InObjectMetadata(metadata)); assertFalse(ContentMetadataDecodingStrategy.isV3InObjectMetadata(metadata)); assertEquals(metadata.get(MetadataKeyConstants.CONTENT_CIPHER), AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName()); @@ -251,7 +251,7 @@ public void testCommitmentPolicyRequireEncryptAllowDecrypt() { //= type=test //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment. assertTrue(ContentMetadataDecodingStrategy.isV3InObjectMetadata(metadata)); - assertFalse(ContentMetadataDecodingStrategy.isV1V2InObjectMetadata(metadata)); + assertFalse(ContentMetadataDecodingStrategy.isV1InObjectMetadata(metadata) || ContentMetadataDecodingStrategy.isV2InObjectMetadata(metadata)); assertEquals(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_V3), AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.idAsString()); //= specification/s3-encryption/key-commitment.md#commitment-policy @@ -323,7 +323,7 @@ public void testCommitmentPolicyRequireEncryptRequireDecrypt() { //= type=test //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment. assertTrue(ContentMetadataDecodingStrategy.isV3InObjectMetadata(metadata)); - assertFalse(ContentMetadataDecodingStrategy.isV1V2InObjectMetadata(metadata)); + assertFalse(ContentMetadataDecodingStrategy.isV1InObjectMetadata(metadata) || ContentMetadataDecodingStrategy.isV2InObjectMetadata(metadata)); assertEquals(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_V3), AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.idAsString()); //= specification/s3-encryption/key-commitment.md#commitment-policy diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java index 3020f5570..3c997591d 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java @@ -445,7 +445,7 @@ public void testExclusiveKeysCollision() { .build(); S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> decodingStrategy.decode(getObjectRequest, response)); - assertTrue(exception.getMessage().contains("Content metadata is tampered, required metadata to decrypt the object are missing")); + assertTrue(exception.getMessage().contains("Content metadata is tampered, required metadata combination is illegal")); } @Test From 7815334eb052f0404ed5123484d8c105cde4f553 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:37:21 -0600 Subject: [PATCH 09/19] fix(CBC): Add annotations for V1/V2 format requirements and fix CBC tag length bug - Add annotation for V1 format exclusive key requirement in MetadataKeyConstants - Fix bug: tag length was incorrectly written for CBC (should only be for GCM) - Add annotations for both GCM and CBC tag length requirements in ContentMetadataEncodingStrategy --- .../s3/internal/ContentMetadataDecodingStrategy.java | 9 ++++++--- .../s3/internal/ContentMetadataEncodingStrategy.java | 8 +++++++- .../encryption/s3/internal/MetadataKeyConstants.java | 5 ++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 04f0c16ab..018e39578 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -422,8 +422,11 @@ public static boolean isV1InObjectMetadata(Map objectMetadata) { //# - If the metadata contains "x-amz-iv" and "x-amz-key" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V1 format. return objectMetadata.containsKey(MetadataKeyConstants.CONTENT_IV) && objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) - && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2) - && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3); + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //= type=implication + //# - Mapkeys exclusive to other format versions MUST NOT be present. + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2) + && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3); } /** @@ -523,7 +526,7 @@ public ContentMetadata decode(GetObjectRequest request, GetObjectResponse respon if (objectMetadata != null) { if (MetadataKeyConstants.hasExclusiveKeyCollision(objectMetadata)) { - throw new S3EncryptionClientException("Content metadata is tampered, required metadata to decrypt the object are missing"); + throw new S3EncryptionClientException("Content metadata is tampered, required metadata combination is illegal."); } // V1/V2 in Object Metadata - All V1/V2 keys present in object metadata diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java index 13efa210a..ffd577f46 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java @@ -187,7 +187,13 @@ private Map addMetadataToMap(Map map, Encryption metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2, ENCODER.encodeToString(edk.encryptedDatakey())); metadata.put(MetadataKeyConstants.CONTENT_IV, ENCODER.encodeToString(iv)); metadata.put(MetadataKeyConstants.CONTENT_CIPHER, materials.algorithmSuite().cipherName()); - metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits())); + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - If the object is encrypted using AES-GCM for content encryption, then the the mapkey "x-amz-tag-len" MUST be present. + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - If the object is encrypted using AES-CBC for content encryption, then the the mapkey "x-amz-tag-len" MUST NOT be present. + if (materials.algorithmSuite().cipherName().contains("GCM")) { + metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits())); + } metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM, edk.keyProviderInfo()); try (JsonWriter jsonWriter = JsonWriter.create()) { diff --git a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java index 61b30feb6..af0b50254 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java @@ -72,7 +72,10 @@ public static boolean isV1Format(Map metadata) { return metadata.containsKey(CONTENT_IV) && metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && - !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //= type=implication + //# - Mapkeys exclusive to other format versions MUST NOT be present. + !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); } From 0d1badb0d97c3123eb206d9573f0666b3a9ef7c3 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:48:15 -0600 Subject: [PATCH 10/19] refactor(duvet): Content Metadata Encoding for Duvet --- .../ContentMetadataDecodingStrategy.java | 17 ++++++++++++----- .../ContentMetadataEncodingStrategy.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 018e39578..9b8e1b3be 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -254,10 +254,20 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. if (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)) { + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - Mapkeys exclusive to other format versions MUST NOT be present. + if (isV2InObjectMetadata(metadata) || isV3InObjectMetadata(metadata)) { + throw new S3EncryptionClientException("Object metadata is tampered, conflicting keys are present."); + } //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# - The mapkey "x-amz-key" MUST be present for V1 format objects. edkCiphertext = DECODER.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)); } else if (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2)) { + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - If a mapkey exclusive to one or more other format versions is present, the S3EC SHOULD throw an exception. + if (isV1InObjectMetadata(metadata) || isV3InObjectMetadata(metadata)) { + throw new S3EncryptionClientException("Object metadata is tampered, conflicting keys are present."); + } //= specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys @@ -300,7 +310,6 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get break; case ALG_AES_256_GCM_IV12_TAG16_NO_KDF: case ALG_AES_256_CTR_IV16_TAG16_NO_KDF: - final int tagLength = Integer.parseInt(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH)); if (tagLength != algorithmSuite.cipherTagLengthBits()) { throw new S3EncryptionClientException("Expected tag length (bits) of: " @@ -520,11 +529,9 @@ public ContentMetadata decodeV3FromInstructionFile(GetObjectRequest request, Get public ContentMetadata decode(GetObjectRequest request, GetObjectResponse response) { Map objectMetadata = response.metadata(); - - //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status - //# If there are multiple mapkeys which are meant to be exclusive to different versions, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. - if (objectMetadata != null) { + //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status + //# If there are multiple mapkeys which are meant to be exclusive to different versions, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception. if (MetadataKeyConstants.hasExclusiveKeyCollision(objectMetadata)) { throw new S3EncryptionClientException("Content metadata is tampered, required metadata combination is illegal."); } diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java index ffd577f46..0d3cefb71 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.Map; +import edu.umd.cs.findbugs.annotations.NonNull; import software.amazon.awssdk.protocols.jsoncore.JsonWriter; import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -178,21 +179,32 @@ private Map addMetadataToMapV3(Map map, Encrypti } return metadata; } + private Map addMetadataToMap(Map map, EncryptionMaterials materials, byte[] iv) { if (materials.algorithmSuite().isCommitting()) { return addMetadataToMapV3(map, materials, iv); } + return addMetadataToMapV2(map, materials, iv); + } + + @NonNull + private Map addMetadataToMapV2(Map map, EncryptionMaterials materials, byte[] iv) { Map metadata = new HashMap<>(map); EncryptedDataKey edk = materials.encryptedDataKeys().get(0); metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2, ENCODER.encodeToString(edk.encryptedDatakey())); metadata.put(MetadataKeyConstants.CONTENT_IV, ENCODER.encodeToString(iv)); metadata.put(MetadataKeyConstants.CONTENT_CIPHER, materials.algorithmSuite().cipherName()); + // When the object is encrypted using the V2 format: + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - The mapkey "x-amz-tag-len" MAY be present for V2 format objects. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# - If the object is encrypted using AES-GCM for content encryption, then the the mapkey "x-amz-tag-len" MUST be present. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# - If the object is encrypted using AES-CBC for content encryption, then the the mapkey "x-amz-tag-len" MUST NOT be present. if (materials.algorithmSuite().cipherName().contains("GCM")) { metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits())); + } else { + throw new S3EncryptionClientException("Only AES-GCM encryption is supported for encryption. AES-CBC is deprecated."); } metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM, edk.keyProviderInfo()); @@ -213,6 +225,9 @@ private Map addMetadataToMap(Map map, Encryption } catch (JsonWriter.JsonGenerationException e) { throw new S3EncryptionClientException("Cannot serialize encryption context to JSON.", e); } + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //= type=exception + //# - The mapkey "x-amz-unencrypted-content-length" MAY be present for V2 format objects. return metadata; } } From c8da8905b25a765ec4323f4f86bfb001d007f775 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:04:05 -0600 Subject: [PATCH 11/19] chore: bump spec --- specification | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification b/specification index ee5d97ae1..fe1591938 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit ee5d97ae109395752273501373f9ca800bcf3bcf +Subproject commit fe159193851d4bc5640c9c6f892eef59c899292e From 4cbfed479fcaae689cf44845f8057261c71e51d4 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:44:40 -0600 Subject: [PATCH 12/19] chore(duvet): bump duvet --- specification | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification b/specification index fe1591938..1365a4713 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit fe159193851d4bc5640c9c6f892eef59c899292e +Subproject commit 1365a471364cb9702d5e102adbf2a1c905f01af9 From d5d73d4f459d27da57bf0775dcffb64587714138 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:44:54 -0600 Subject: [PATCH 13/19] chore(duvet): CBC Decryption --- .../amazon/encryption/s3/internal/CipherProvider.java | 4 ++++ .../amazon/encryption/s3/internal/CryptoFactory.java | 7 +++++++ .../encryption/s3/internal/GetEncryptedObjectPipeline.java | 3 +++ 3 files changed, 14 insertions(+) diff --git a/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java b/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java index f6e2aa53e..99562f32f 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java +++ b/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java @@ -193,6 +193,10 @@ public static Cipher createAndInitCipher(final CryptographicMaterials materials, if (materials.cipherMode().opMode() == Cipher.ENCRYPT_MODE) { throw new S3EncryptionClientException("Encryption is not supported for algorithm: " + materials.algorithmSuite().cipherName()); } + //= specification/s3-encryption/decryption.md#cbc-decryption + //# If an object is encrypted with ALG_AES_256_CBC_IV16_NO_KDF and [legacy unauthenticated algorithm suites](#legacy-decryption) is enabled, + //# then the S3EC MUST create a cipher object using the cipher transformation "AES/CBC/PKCS5Padding". + // NOTE: PKCS5Padding is specified above in CryptoFactory.createCipher(materials.algorithmSuite().cipherName(), materials.cryptoProvider()) cipher.init(materials.cipherMode().opMode(), materials.dataKey(), new IvParameterSpec(iv)); break; case ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY: diff --git a/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java b/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java index e34ba3c6e..7eed70aba 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java +++ b/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java @@ -11,6 +11,13 @@ import java.security.Provider; public class CryptoFactory { + //= specification/s3-encryption/decryption.md#cbc-decryption + //# If the cipher object cannot be created as described above, + //# Decryption MUST fail. + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=exception + //# The error SHOULD detail why the cipher could not be initialized + //# (such as CBC or PKCS5Padding is not supported by the underlying crypto provider). public static Cipher createCipher(String algorithm, Provider provider) throws NoSuchPaddingException, NoSuchAlgorithmException { // if the user has specified a provider, go with that. diff --git a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java index 92fd59019..c298b3636 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java +++ b/src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java @@ -96,6 +96,9 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g //= specification/s3-encryption/decryption.md#legacy-decryption //# If the S3EC is not configured to enable legacy unauthenticated content decryption, the client MUST throw //# an exception when attempting to decrypt an object encrypted with a legacy unauthenticated algorithm suite. + //= specification/s3-encryption/decryption.md#cbc-decryption + //# If an object is encrypted with ALG_AES_256_CBC_IV16_NO_KDF and [legacy unauthenticated algorithm suites](#legacy-decryption) is NOT enabled, + //# the S3EC MUST throw an error which details that client was not configured to decrypt objects with ALG_AES_256_CBC_IV16_NO_KDF. throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use legacy content decryption: " + algorithmSuite.cipherName()); } From 044e80d331781f9785fd58cfbad32fe27e7cc845 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:32:22 -0600 Subject: [PATCH 14/19] docs(duvet): Add annotation for V3 exclusive key requirement Add Duvet annotation for requirement: 'If a mapkey exclusive to other (non-V3) format versions is present, the S3EC SHOULD throw an exception.' Implementation: MetadataKeyConstants.isV3Format() checks that V1/V2 keys are not present when validating V3 format at ContentMetadataDecodingStrategy line 92. Test coverage: Existing testExclusiveKeysCollision validates exception is thrown for conflicting version keys. --- CONTEXT_FOR_DUVET.md | 56 +++++++++++++++++++ .../ContentMetadataDecodingStrategy.java | 9 ++- 2 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 CONTEXT_FOR_DUVET.md diff --git a/CONTEXT_FOR_DUVET.md b/CONTEXT_FOR_DUVET.md new file mode 100644 index 000000000..ba28f3c6b --- /dev/null +++ b/CONTEXT_FOR_DUVET.md @@ -0,0 +1,56 @@ +# Duvet Annotation Context + +## Overview +Working through incomplete Duvet requirements one at a time to add citations and tests to the codebase. + +## Key Insights +- **Duvet syntax**: Only comments with exactly two slashes (`//`) are read by Duvet + - `//=` for specification links + - `//#` for requirement descriptions + - `///` or `////` are NOT read by Duvet (effectively commented out) +- **Annotation types**: `type=implication`, `type=exception`, `type=test` +- **Report location**: `.duvet/reports/report.html` +- **Total requirements**: 198 (178 complete, 20 incomplete as of last run) + +## Completed Requirements + +### 1. V1 Format Exclusive Keys +**Requirement**: "When the object is encrypted using the V1 format, - Mapkeys exclusive to other format versions MUST NOT be present." + +**Citation added**: +- `MetadataKeyConstants.isV1Format()` at line 73 +- `ContentMetadataDecodingStrategy.isV1InObjectMetadata()` at line 424 + +**Test**: Already tested via `ContentMetadataStrategyTest.testExclusiveKeysCollision` + +### 2. V2 Format Tag Length Requirements +**Requirements**: +- "If the object is encrypted using AES-GCM for content encryption, then the mapkey 'x-amz-tag-len' MUST be present." +- "If the object is encrypted using AES-CBC for content encryption, then the mapkey 'x-amz-tag-len' MUST NOT be present." + +**Bug fixed**: Code was incorrectly writing tag length for CBC (should only be for GCM) + +**Citation added**: `ContentMetadataEncodingStrategy.addMetadataToMap()` at line 190 + +**Implementation**: Check `cipherName().contains("GCM")` to determine whether to write tag length + +**Test**: Existing tests validate tag length is written for GCM and read correctly + +## Commit History +- `9068c876`: fix(CBC): Add annotations for V1/V2 format requirements and fix CBC tag length bug + +## Files Modified +- `src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java` +- `src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java` +- `src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java` + +## Process for Next Requirements +1. User provides incomplete requirement text +2. Find where requirement is implemented in code +3. Add Duvet annotation at implementation location +4. Verify test coverage exists +5. Run tests: `mvn clean compile` then `mvn test -Dtest=ContentMetadataStrategyTest,MetadataKeyConstantsTest,CipherProviderTest,AlgorithmSuiteValidationTest` +6. Stage and commit changes + +## Remaining Work +18 incomplete requirements remaining (as of last count) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 9b8e1b3be..847ca9e8f 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -92,6 +92,8 @@ private static ContentMetadata readFromV3FormatMap(Map metadata, if (!MetadataKeyConstants.isV3Format(metadata)) { //= specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status //# In general, if there is any deviation from the above format, with the exception of additional unrelated mapkeys, then the S3EC SHOULD throw an exception. + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //# - If a mapkey exclusive to other (non-V3) format versions is present, the S3EC SHOULD throw an exception. throw new S3EncryptionClientException("Content metadata is tampered, required metadata to decrypt the object are missing"); } @@ -255,7 +257,7 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. if (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)) { //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //# - Mapkeys exclusive to other format versions MUST NOT be present. + //# - If mapkeys exclusive to other (non-V1) format versions is present,the S3EC SHOULD throw an exception. if (isV2InObjectMetadata(metadata) || isV3InObjectMetadata(metadata)) { throw new S3EncryptionClientException("Object metadata is tampered, conflicting keys are present."); } @@ -264,7 +266,7 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get edkCiphertext = DECODER.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)); } else if (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2)) { //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //# - If a mapkey exclusive to one or more other format versions is present, the S3EC SHOULD throw an exception. + //# - If a mapkey exclusive to other (non-V2) format versions is present, the S3EC SHOULD throw an exception. if (isV1InObjectMetadata(metadata) || isV3InObjectMetadata(metadata)) { throw new S3EncryptionClientException("Object metadata is tampered, conflicting keys are present."); } @@ -431,9 +433,6 @@ public static boolean isV1InObjectMetadata(Map objectMetadata) { //# - If the metadata contains "x-amz-iv" and "x-amz-key" but no other version exclusive keys then the object MUST be considered as an S3EC-encrypted object using the V1 format. return objectMetadata.containsKey(MetadataKeyConstants.CONTENT_IV) && objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1) - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //= type=implication - //# - Mapkeys exclusive to other format versions MUST NOT be present. && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2) && !objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3); } From 1e10b4258fd566c48a932af9be44f8a0baf6c5c1 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:43:05 -0600 Subject: [PATCH 15/19] docs(duvet): Add annotations for V2 unencrypted-content-length requirement Add Duvet annotations for requirement: 'The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V2 format objects.' Annotations marked as type=exception since the implementation does not write or check this field for V2 objects. Locations: - ContentMetadataDecodingStrategy line 277-279 (V2 decoding) - ContentMetadataEncodingStrategy line 228-230 (V2 encoding) Test coverage: Existing V2 format tests validate decoding without this field. --- .../s3/internal/ContentMetadataDecodingStrategy.java | 3 +++ .../s3/internal/ContentMetadataEncodingStrategy.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java index 847ca9e8f..60fc4ac56 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataDecodingStrategy.java @@ -274,6 +274,9 @@ private static ContentMetadata readFromMapV1V2(Map metadata, Get //# Objects encrypted with ALG_AES_256_CBC_IV16_NO_KDF MAY use either the V1 or V2 message format version. //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //# - The mapkey "x-amz-key-v2" MUST be present for V2 format objects. + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //= type=exception + //# - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V2 format objects. edkCiphertext = DECODER.decode(metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2)); } else { // this shouldn't happen under normal circumstances- only if out-of-band modification diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java index 0d3cefb71..334e9bb9c 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java @@ -227,7 +227,7 @@ private Map addMetadataToMapV2(Map map, Encrypti } //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys //= type=exception - //# - The mapkey "x-amz-unencrypted-content-length" MAY be present for V2 format objects. + //# - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V2 format objects. return metadata; } } From 97ff0cae2656c0880ba8c924afb882bde6e47595 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:36:42 -0600 Subject: [PATCH 16/19] chore(spec): bump spec for code review suggestions --- specification | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification b/specification index 1365a4713..5c1235163 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 1365a471364cb9702d5e102adbf2a1c905f01af9 +Subproject commit 5c123516355e101f2eed8625eb0e5d4694376a00 From 1f7b084d62f4eb93191479a5e39223bcb575f8ee Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:37:04 -0600 Subject: [PATCH 17/19] chore(duvet): duvet for updated spec --- .../s3/internal/CipherProvider.java | 11 +++++++++-- .../ContentMetadataEncodingStrategy.java | 16 ++++++++++++++++ .../encryption/s3/internal/CryptoFactory.java | 7 ------- .../s3/internal/MetadataKeyConstants.java | 6 +++--- ...EncryptionClientBuilderValidationTest.java | 4 ++++ .../s3/internal/CipherProviderTest.java | 15 +++++++++++++++ .../internal/ContentMetadataStrategyTest.java | 19 +++++++++++++++++++ 7 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java b/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java index 99562f32f..2bb23c037 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java +++ b/src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java @@ -195,8 +195,15 @@ public static Cipher createAndInitCipher(final CryptographicMaterials materials, } //= specification/s3-encryption/decryption.md#cbc-decryption //# If an object is encrypted with ALG_AES_256_CBC_IV16_NO_KDF and [legacy unauthenticated algorithm suites](#legacy-decryption) is enabled, - //# then the S3EC MUST create a cipher object using the cipher transformation "AES/CBC/PKCS5Padding". - // NOTE: PKCS5Padding is specified above in CryptoFactory.createCipher(materials.algorithmSuite().cipherName(), materials.cryptoProvider()) + //# then the S3EC MUST create a cipher with AES in CBC Mode with PKCS5Padding or PKCS7Padding compatible padding for a 16-byte block cipher (example: for the Java JCE, this is "AES/CBC/PKCS5Padding"). + // NOTE: CBC and PKCS5Padding is specified above in CryptoFactory.createCipher(materials.algorithmSuite().cipherName(), materials.cryptoProvider()) + //= specification/s3-encryption/decryption.md#cbc-decryption + //# If the cipher object cannot be created as described above, + //# Decryption MUST fail. + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=exception + //# The error SHOULD detail why the cipher could not be initialized + //# (such as CBC or PKCS5Padding is not supported by the underlying crypto provider). cipher.init(materials.cipherMode().opMode(), materials.dataKey(), new IvParameterSpec(iv)); break; case ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY: diff --git a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java index 334e9bb9c..39696f1cb 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java +++ b/src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java @@ -54,6 +54,11 @@ public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, } else { //= specification/s3-encryption/data-format/metadata-strategy.md#object-metadata //# By default, the S3EC MUST store content metadata in the S3 Object Metadata. + //= specification/s3-encryption/data-format/content-metadata.md#us-ascii-preferred-string + //= type=exception + //= reason=It would be a breaking change to introduce this. + //# Thus, + //# Content Metadata MapKeys SHOULD be restricted to US-ASCII. Map newMetadata = addMetadataToMap(putObjectRequest.metadata(), materials, iv); return putObjectRequest.toBuilder() .metadata(newMetadata) @@ -80,6 +85,11 @@ public CreateMultipartUploadRequest encodeMetadata(EncryptionMaterials materials return createMultipartUploadRequest.toBuilder() .metadata(objectMetadata).build(); } else { + //= specification/s3-encryption/data-format/content-metadata.md#us-ascii-preferred-string + //= type=exception + //= reason=It would be a breaking change to introduce this. + //# Thus, + //# Content Metadata MapKeys SHOULD be restricted to US-ASCII. Map newMetadata = addMetadataToMap(createMultipartUploadRequest.metadata(), materials, iv); return createMultipartUploadRequest.toBuilder() .metadata(newMetadata) @@ -159,6 +169,8 @@ private Map addMetadataToMapV3(Map map, Encrypti jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); } jsonWriter.writeEndObject(); + //= specification/s3-encryption/data-format/content-metadata.md#us-ascii-preferred-string + //# An implementation MAY support UTF-8. String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files //# - The V3 message format MUST store the mapkey "x-amz-t" and its value (when present in the content metadata) in the Instruction File. @@ -169,6 +181,8 @@ private Map addMetadataToMapV3(Map map, Encrypti jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); } jsonWriter.writeEndObject(); + //= specification/s3-encryption/data-format/content-metadata.md#us-ascii-preferred-string + //# An implementation MAY support UTF-8. String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files //# - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File. @@ -220,6 +234,8 @@ private Map addMetadataToMapV2(Map map, Encrypti } } jsonWriter.writeEndObject(); + //= specification/s3-encryption/data-format/content-metadata.md#us-ascii-preferred-string + //# An implementation MAY support UTF-8. String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_MATDESC_OR_EC, jsonEncryptionContext); } catch (JsonWriter.JsonGenerationException e) { diff --git a/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java b/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java index 7eed70aba..e34ba3c6e 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java +++ b/src/main/java/software/amazon/encryption/s3/internal/CryptoFactory.java @@ -11,13 +11,6 @@ import java.security.Provider; public class CryptoFactory { - //= specification/s3-encryption/decryption.md#cbc-decryption - //# If the cipher object cannot be created as described above, - //# Decryption MUST fail. - //= specification/s3-encryption/decryption.md#cbc-decryption - //= type=exception - //# The error SHOULD detail why the cipher could not be initialized - //# (such as CBC or PKCS5Padding is not supported by the underlying crypto provider). public static Cipher createCipher(String algorithm, Provider provider) throws NoSuchPaddingException, NoSuchAlgorithmException { // if the user has specified a provider, go with that. diff --git a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java index af0b50254..38e15039b 100644 --- a/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java +++ b/src/main/java/software/amazon/encryption/s3/internal/MetadataKeyConstants.java @@ -72,9 +72,9 @@ public static boolean isV1Format(Map metadata) { return metadata.containsKey(CONTENT_IV) && metadata.containsKey(ENCRYPTED_DATA_KEY_MATDESC_OR_EC) && metadata.containsKey(ENCRYPTED_DATA_KEY_V1) && - //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys - //= type=implication - //# - Mapkeys exclusive to other format versions MUST NOT be present. + ///= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + ///= type=implication + ///# - Mapkeys exclusive to other format versions MUST NOT be present. !metadata.containsKey(ENCRYPTED_DATA_KEY_V2) && !metadata.containsKey(ENCRYPTED_DATA_KEY_V3); } diff --git a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientBuilderValidationTest.java b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientBuilderValidationTest.java index 9d39c4ebf..c544035f3 100644 --- a/src/test/java/software/amazon/encryption/s3/S3EncryptionClientBuilderValidationTest.java +++ b/src/test/java/software/amazon/encryption/s3/S3EncryptionClientBuilderValidationTest.java @@ -181,6 +181,10 @@ public void testBuilderWithLegacyAlgorithmFails() throws NoSuchAlgorithmExceptio S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4() .aesKey(aesKey) + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=test + //# If an object is encrypted with ALG_AES_256_CBC_IV16_NO_KDF and [legacy unauthenticated algorithm suites](#legacy-decryption) is NOT enabled, + //# the S3EC MUST throw an error which details that client was not configured to decrypt objects with ALG_AES_256_CBC_IV16_NO_KDF. .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF) .build() ); diff --git a/src/test/java/software/amazon/encryption/s3/internal/CipherProviderTest.java b/src/test/java/software/amazon/encryption/s3/internal/CipherProviderTest.java index 92fc79d89..464abe2e9 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/CipherProviderTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/CipherProviderTest.java @@ -300,6 +300,21 @@ public void testCreateAndInitCipherALG_AES_256_CBC_IV16_NO_KDF_DecryptionSucceed Cipher cipher = CipherProvider.createAndInitCipher(materials, iv, messageId); assertNotNull(cipher); + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=exception + //= reason=Well cipher creation failure does throw an error, we do not catch the error and throw a reasonable message + //# If the cipher object cannot be created as described above, + //# Decryption MUST fail. + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=TODO + //# The error SHOULD detail why the cipher could not be initialized + //# (such as CBC or PKCS5Padding is not supported by the underlying crypto provider). + // If we ever refactor so that the cipher creation failure is a modeled error, we should add tests for it. + + //= specification/s3-encryption/decryption.md#cbc-decryption + //= type=test + //# If an object is encrypted with ALG_AES_256_CBC_IV16_NO_KDF and [legacy unauthenticated algorithm suites](#legacy-decryption) is enabled, + //# then the S3EC MUST create a cipher with AES in CBC Mode with PKCS5Padding or PKCS7Padding compatible padding for a 16-byte block cipher (example: for the Java JCE, this is "AES/CBC/PKCS5Padding"). assertEquals("AES/CBC/PKCS5Padding", cipher.getAlgorithm()); } diff --git a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java index 3c997591d..a0fda00e8 100644 --- a/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java +++ b/src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java @@ -427,6 +427,25 @@ public void testMissingKeysV1() { assertTrue(exception.getMessage().contains("Content metadata is tampered, required metadata to decrypt the object are missing")); } + @Test + public void testMissingKeysV1Colliding() { + Map metadata = new HashMap<>(); + metadata.put("x-amz-iv", "dGVzdC1pdi0xMi1i"); // base64 of "test-iv-12-b" + metadata.put("x-amz-key", "ZW5jcnlwdGVkLWtleS1kYXRh"); // base64 of "encrypted-key-data" + metadata.put("x-amz-matdesc", "{}"); + metadata.put("x-amz-key-v2", "ZW5jcnlwdGVkLWtleS1kYXRh"); + + GetObjectResponse response = GetObjectResponse.builder() + .metadata(metadata) + .build(); + //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys + //= type=test + //# - If mapkeys exclusive to other (non-V1) format versions is present,the S3EC SHOULD throw an exception. + S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> decodingStrategy.decode(getObjectRequest, response)); + System.out.println(exception.getMessage()); + assertTrue(exception.getMessage().contains("Content metadata is tampered, required metadata combination is illegal.")); + } + @Test public void testExclusiveKeysCollision() { Map metadata = new HashMap<>(); From 256218ea814d7cd05095ae6cf11e15dfda3f96f2 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:55:18 -0600 Subject: [PATCH 18/19] chore(Duvet): update spec for Key Commitment --- .gitmodules | 4 ++-- specification | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 9dd945251..68883895d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "private_aws"] path = specification - url = git@github.com:awslabs/private-aws-encryption-sdk-specification-staging.git - branch = tonyknap/v4.0.1-candidate + url = https://github.com/awslabs/aws-encryption-sdk-specification.git + branch = tonyknap/s3ec-v3.0.1-candidate diff --git a/specification b/specification index 5c1235163..2e1710a6b 160000 --- a/specification +++ b/specification @@ -1 +1 @@ -Subproject commit 5c123516355e101f2eed8625eb0e5d4694376a00 +Subproject commit 2e1710a6b305484612951bc97985fd15c80f5823 From 28c52c6917b9f37a07794e07a9d6686a62138837 Mon Sep 17 00:00:00 2001 From: texastony <5892063+texastony@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:36:16 -0600 Subject: [PATCH 19/19] Trigger CI build