VYPR
Medium severity5.3OSV Advisory· Published Dec 17, 2025· Updated Apr 15, 2026

CVE-2025-14763

CVE-2025-14763

Description

Missing cryptographic key commitment in the Amazon S3 Encryption Client for Java may allow a user with write access to the S3 bucket to introduce a new EDK that decrypts to different plaintext when the encrypted data key is stored in an "instruction file" instead of S3's metadata record.

To mitigate this issue, upgrade Amazon S3 Encryption Client for Java to version 4.0.0 or later.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
software.amazon.encryption.s3:amazon-s3-encryption-client-javaMaven
< 4.0.04.0.0

Affected products

1

Patches

2
9d4523edbbc2

feat!: Updates to the S3 Encryption Client (#491)

63 files changed · +5073 866
  • .github/workflows/build.yml+1 1 modified
    @@ -59,4 +59,4 @@ jobs:
           - name: Package JAR
             run: |
               mvn -B -ntp install -DskipTests
    -        shell: bash
    +        shell: bash
    \ No newline at end of file
    
  • .github/workflows/ci-workflow.yml+7 0 modified
    @@ -22,3 +22,10 @@ jobs:
         with:
           version: ${{ matrix.version }}
           distribution: ${{ matrix.distribution }}
    +
    +  Examples:
    +    uses: ./.github/workflows/examples.yml
    +    secrets: inherit
    +    with:
    +      version: 17
    +      distribution: corretto
    
  • .github/workflows/examples.yml+53 0 added
    @@ -0,0 +1,53 @@
    +name: Migration Examples
    +
    +on:
    +  workflow_call:
    +    inputs:
    +      version:
    +        required: true
    +        type: string
    +      distribution:
    +        required: true
    +        type: string
    +
    +jobs:
    +  MigrationExamples:
    +    runs-on: ubuntu-latest
    +    permissions:
    +      id-token: write
    +      contents: read
    +
    +    steps:
    +      - name: Configure AWS Credentials
    +        uses: aws-actions/configure-aws-credentials@v2
    +        with:
    +          role-to-assume: arn:aws:iam::${{ secrets.CI_AWS_ACCOUNT_ID }}:role/service-role/${{ vars.CI_AWS_ROLE }}
    +          role-session-name: S3EC-Github-CI-Tests
    +          aws-region: ${{ vars.CI_AWS_REGION }}
    +
    +      - name: Checkout Code
    +        uses: actions/checkout@v3
    +
    +      - name: Setup JDK
    +        uses: actions/setup-java@v3
    +        with:
    +          distribution: ${{ inputs.distribution }}
    +          java-version: ${{ inputs.version}}
    +          cache: 'maven'
    +
    +      - name: Install S3EC
    +        run: |
    +          mvn -B -ntp install -DskipTests
    +        shell: bash
    +
    +      # TODO: Add step to run v3 examples once we have transitional version
    +      #       cd migration_examples/v3-to-v4/v3
    +      #       mvn -B -ntp test -DskipCompile
    +      - name: Run Migration Examples
    +        run: |
    +          export AWS_S3EC_TEST_BUCKET=${{ vars.CI_S3_BUCKET }}
    +          export AWS_S3EC_TEST_KMS_KEY_ID=arn:aws:kms:${{ vars.CI_AWS_REGION }}:${{ secrets.CI_AWS_ACCOUNT_ID }}:key/${{ vars.CI_KMS_KEY_ID }}
    +          export AWS_REGION=${{ vars.CI_AWS_REGION }}
    +          cd migration_examples/v3-to-v4/v4
    +          mvn -B -ntp test -DskipCompile
    +        shell: bash
    
  • migration_examples/v3-to-v4/README.md+52 0 added
    @@ -0,0 +1,52 @@
    +# S3 Encryption Client v3 to v4 Migration Examples
    +
    +> **Note:** This directory contains migration-specific examples for users upgrading from v3 to v4. If you're starting fresh with the S3 Encryption Client, see the [code examples](../../../src/examples/java/software/amazon/encryption/s3/examples).
    +
    +This directory contains examples demonstrating the migration path from S3 Encryption Client v3 to v4, focusing on different commitment policy configurations. For more information, refer to the <a href="https://docs.aws.amazon.com/amazon-s3-encryption-client/latest/developerguide/java-v4-migration.html">Developer Guide</a>.
    +
    +These examples are for users who have already stored data using the S3 Encryption Client (S3EC) for Java v3 or earlier versions
    +(or are using implementations of the S3EC in other languages)
    +and want to migrate their data to use the v4 client and key committing algorithms.
    +If you are not already using the S3EC, you can simply use the default configuration of the latest version of the S3EC Java
    +and be sure you are reading and writing objects encrypted using key committing algorithms.
    +
    +## Directory Structure
    +
    +- `v3/` - v3 client that writes objects (`PutObject`) encrypted with non-key committing algorithms and reads objects (`GetObject`) encrypted with either key committing or non-key committing algorithms
    +- `v4/` - v4 client examples with different commitment policies
    +  - `V4MigrationStep1.java` - v4 client that writes objects encrypted with non-key committing algorithms and reads objects encrypted with either key committing or non-key committing algorithms
    +  - `V4MigrationStep2.java` - v4 client that writes objects encrypted with **key committing algorithms** and reads objects encrypted with either key committing or non-key committing algorithms
    +  - `V4MigrationStep3.java` - v4 client that writes objects encrypted with key committing algorithms and reads objects encrypted with **only key committing algorithms**
    +
    +## Running the Examples
    +
    +Each example is a standalone Java program that can be run using Maven:
    +
    +```bash
    +# V3 baseline
    +cd v3
    +mvn exec:java -Dexec.args="<bucket-name> <object-key> <kms-key-id> <region>"
    +
    +# V4 examples
    +cd ../v4
    +mvn exec:java -Dexec.mainClass="software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep1" -Dexec.args="<bucket-name> <object-key> <kms-key-id> <region>"
    +
    +mvn exec:java -Dexec.mainClass="software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep2" -Dexec.args="<bucket-name> <object-key> <kms-key-id> <region>"
    +
    +mvn exec:java -Dexec.mainClass="software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep3" -Dexec.args="<bucket-name> <object-key> <kms-key-id> <region>"
    +```
    +
    +## Key Commitment Policies
    +
    +The examples demonstrate different commitment policies that dictate the support algorithm types on encrypt and decrypt:
    +
    +- **FORBID_ENCRYPT_ALLOW_DECRYPT**: Does not use key commitment on encrypt (`PutObject`), can decrypt objects (`GetObject`) with or without key commitment
    +- **REQUIRE_ENCRYPT_ALLOW_DECRYPT**: Uses key commitment on encrypt, can decrypt objects with or without key commitment
    +- **REQUIRE_ENCRYPT_REQUIRE_DECRYPT**: Uses key commitment on encrypt, can only decrypt objects that also use key commitment
    +
    +## Prerequisites
    +
    +- Java 8 or later
    +- AWS credentials configured
    +- S3 bucket for testing
    +- KMS key for encryption
    
  • migration_examples/v3-to-v4/v3/pom.xml+113 0 added
    @@ -0,0 +1,113 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0"
    +         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <groupId>software.amazon.encryption.s3.examples</groupId>
    +    <artifactId>migration-v3-baseline</artifactId>
    +    <version>1.0.0</version>
    +    <packaging>jar</packaging>
    +
    +    <name>S3 Encryption Client Migration Step 0 (V3 Baseline)</name>
    +    <description>Migration Step 0: V3 baseline example for S3 Encryption Client migration</description>
    +
    +    <properties>
    +        <maven.compiler.source>8</maven.compiler.source>
    +        <maven.compiler.target>8</maven.compiler.target>
    +        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +        <aws.sdk.version>2.31.14</aws.sdk.version>
    +        <s3ec.version>3.6.0</s3ec.version>
    +    </properties>
    +
    +    <dependencies>
    +        <!-- S3 Encryption Client V3 - This example shows the v3 baseline -->
    +        <dependency>
    +            <groupId>software.amazon.encryption.s3</groupId>
    +            <artifactId>amazon-s3-encryption-client-java</artifactId>
    +            <version>${s3ec.version}</version>
    +        </dependency>
    +
    +        <!-- AWS SDK S3 -->
    +        <dependency>
    +            <groupId>software.amazon.awssdk</groupId>
    +            <artifactId>s3</artifactId>
    +            <version>${aws.sdk.version}</version>
    +        </dependency>
    +
    +        <!-- AWS SDK KMS -->
    +        <dependency>
    +            <groupId>software.amazon.awssdk</groupId>
    +            <artifactId>kms</artifactId>
    +            <version>${aws.sdk.version}</version>
    +        </dependency>
    +
    +        <!-- SAAJ implementation for MIME utilities -->
    +        <dependency>
    +            <groupId>com.sun.xml.messaging.saaj</groupId>
    +            <artifactId>saaj-impl</artifactId>
    +            <version>2.0.1</version>
    +        </dependency>
    +
    +        <!-- JUnit for testing -->
    +        <dependency>
    +            <groupId>org.junit.jupiter</groupId>
    +            <artifactId>junit-jupiter</artifactId>
    +            <version>5.10.0</version>
    +            <scope>test</scope>
    +        </dependency>
    +    </dependencies>
    +
    +    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-compiler-plugin</artifactId>
    +                <version>3.11.0</version>
    +                <configuration>
    +                    <source>8</source>
    +                    <target>8</target>
    +                </configuration>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.codehaus.mojo</groupId>
    +                <artifactId>exec-maven-plugin</artifactId>
    +                <version>3.1.0</version>
    +                <configuration>
    +                    <mainClass>software.amazon.encryption.s3.examples.migration.v3.V3MigrationStep0</mainClass>
    +                </configuration>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-surefire-plugin</artifactId>
    +                <version>3.0.0</version>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-shade-plugin</artifactId>
    +                <version>3.5.1</version>
    +                <executions>
    +                    <execution>
    +                        <phase>package</phase>
    +                        <goals>
    +                            <goal>shade</goal>
    +                        </goals>
    +                        <configuration>
    +                            <transformers>
    +                                <transformer
    +                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
    +                                    <mainClass>software.amazon.encryption.s3.examples.migration.v3.V3MigrationStep0
    +                                    </mainClass>
    +                                </transformer>
    +                            </transformers>
    +                            <finalName>v3-migration-step0</finalName>
    +                        </configuration>
    +                    </execution>
    +                </executions>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</project>
    
  • migration_examples/v3-to-v4/v3/src/main/java/software/amazon/encryption/s3/examples/migration/v3/V3MigrationStep0.java+120 0 added
    @@ -0,0 +1,120 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v3;
    +
    +import software.amazon.awssdk.core.sync.RequestBody;
    +import software.amazon.awssdk.core.sync.ResponseTransformer;
    +import software.amazon.awssdk.regions.Region;
    +import software.amazon.awssdk.services.kms.KmsClient;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.GetObjectRequest;
    +import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    +import software.amazon.encryption.s3.CommitmentPolicy;
    +import software.amazon.encryption.s3.S3EncryptionClient;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    +import software.amazon.encryption.s3.materials.KmsKeyring;
    +
    +/**
    + * Migration Step 0: This example demonstrates use of the S3 Encryption Client for Java v3
    + * and is the starting state for migrating your data to the v4 client.
    + * <p>
    + * This example's purpose is to model behavior of an existing v3 client.
    + * Subsequent migration steps will demonstrate code changes needed to use the v4 client.
    + * <p>
    + * This example configures a v3 client to:
    + * - Write objects using non-key committing encryption algorithms
    + * - Read objects encrypted with either key committing algorithms or with non-key committing algorithms
    + * <p>
    + * In this configuration, the client can read objects encrypted
    + * with non-key committing algorithms (written by this v3 client or an in-progress v4 migration),
    + * as well as objects encrypted by a migrated v4 client
    + * that is configured to write objects encrypted with key committing algorithms.
    + * You should ensure you are using the latest version of the v3 client
    + * that can read objects encrypted with key committing algorithms before proceeding with migration.
    + */
    +public class V3MigrationStep0 {
    +
    +    private static final int CURRENT_MIGRATION_STEP = 0;
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region)
    +            throws Exception {
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region, CURRENT_MIGRATION_STEP);
    +    }
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region,
    +                                           int sourceStep) throws Exception {
    +        // Test data for encryption
    +        String testData = "Hello, World! This is a test message for S3 encryption client migration.";
    +
    +        // Create regular S3 client
    +        S3Client s3Client = S3Client.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS client
    +        KmsClient kmsClient = KmsClient.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS keyring
    +        KmsKeyring keyring = KmsKeyring.builder()
    +                .kmsClient(kmsClient)
    +                .wrappingKeyId(kmsKeyId)
    +                .build();
    +
    +        // Create S3 Encryption Client v3 with FORBID_ENCRYPT_ALLOW_DECRYPT commitment policy (default)
    +        // This is the default commitment policy for v3 clients
    +        // that can read objects encrypted with key commitment;
    +        // you do not need to set this explicitly.
    +        // However, setting this explicitly helps avoid accidental use of a v3 client
    +        // that cannot read objects encrypted with key committing algorithms.
    +        S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4()
    +                .keyring(keyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .build();
    +
    +        // Create object keys for PUT and GET operations
    +        // PUT: Always use current step
    +        String putObjectKey = String.format("%s-step-%d", objectKey, CURRENT_MIGRATION_STEP);
    +        // GET: Use sourceStep (debug parameter to test cross-compatibility between steps; defaults to current step)
    +        String getObjectKey = String.format("%s-step-%d", objectKey, sourceStep);
    +
    +        // Upload encrypted object using S3 Encryption Client
    +        encryptionClient.putObject(
    +                PutObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(putObjectKey)
    +                        .build(),
    +                RequestBody.fromString(testData));
    +
    +        // Download and decrypt object using S3 Encryption Client
    +        String decryptedData = encryptionClient.getObject(
    +                GetObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(getObjectKey)
    +                        .build(),
    +                ResponseTransformer.toBytes()
    +        ).asUtf8String();
    +
    +        // Verify the roundtrip was successful
    +        if (!decryptedData.equals(testData)) {
    +            throw new AssertionError(
    +                    String.format("Roundtrip failed - data mismatch. Original: %s, Decrypted: %s",
    +                            testData, decryptedData));
    +        }
    +
    +        // Clean up resources
    +        encryptionClient.close();
    +        kmsClient.close();
    +    }
    +
    +    public static void main(String[] args) throws Exception {
    +        String bucketName = args[0];
    +        String objectKey = args[1];
    +        String kmsKeyId = args[2];
    +        String region = args[3];
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region);
    +    }
    +}
    
  • migration_examples/v3-to-v4/v3/src/test/java/software/amazon/encryption/s3/examples/migration/v3/V3MigrationStep0Test.java+43 0 added
    @@ -0,0 +1,43 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v3;
    +
    +import org.junit.jupiter.api.AfterAll;
    +import org.junit.jupiter.api.Test;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
    +import software.amazon.awssdk.regions.Region;
    +
    +/**
    + * Test for V3 Migration Step 0 example.
    + * This test requires AWS credentials and resources to be configured via environment variables.
    + */
    +public class V3MigrationStep0Test {
    +
    +    private static final String BUCKET_NAME = System.getenv("AWS_S3EC_TEST_BUCKET");
    +    private static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID");
    +    private static final String REGION = System.getenv("AWS_REGION");
    +    private static final String TEST_OBJECT_KEY = "migration-test-v3-" + ((int) (Math.random() * 100000));
    +
    +    @Test
    +    public void testV3MigrationStep0() throws Exception {
    +        // Successfully executes step 0
    +        // Step 0 writes without key commitment
    +        V3MigrationStep0.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 0);
    +    }
    +
    +    @AfterAll
    +    public static void cleanupTestObjects() {
    +        try (S3Client s3Client = S3Client.builder()
    +                .region(Region.of(REGION))
    +                .build()) {
    +            // Delete object created by step 0
    +            String objectKey = String.format("%s-step-%d", TEST_OBJECT_KEY, 0);
    +            s3Client.deleteObject(DeleteObjectRequest.builder()
    +                    .bucket(BUCKET_NAME)
    +                    .key(objectKey)
    +                    .build());
    +        }
    +    }
    +}
    
  • migration_examples/v3-to-v4/v4/pom.xml+133 0 added
    @@ -0,0 +1,133 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns="http://maven.apache.org/POM/4.0.0"
    +         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <groupId>software.amazon.encryption.s3.examples</groupId>
    +    <artifactId>migration-v4-steps</artifactId>
    +    <version>1.0.0</version>
    +    <packaging>jar</packaging>
    +
    +    <name>S3 Encryption Client Migration Steps 1-3 (V4)</name>
    +    <description>Migration Steps 1-3: V4 client migration examples</description>
    +
    +    <properties>
    +        <maven.compiler.source>8</maven.compiler.source>
    +        <maven.compiler.target>8</maven.compiler.target>
    +        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +        <aws.sdk.version>2.31.14</aws.sdk.version>
    +        <s3ec.version>3.6.0</s3ec.version>
    +    </properties>
    +
    +    <dependencies>
    +        <!-- S3 Encryption Client V4 -->
    +        <dependency>
    +            <groupId>software.amazon.encryption.s3</groupId>
    +            <artifactId>amazon-s3-encryption-client-java</artifactId>
    +            <version>${s3ec.version}</version>
    +        </dependency>
    +
    +        <!-- AWS SDK S3 -->
    +        <dependency>
    +            <groupId>software.amazon.awssdk</groupId>
    +            <artifactId>s3</artifactId>
    +            <version>${aws.sdk.version}</version>
    +        </dependency>
    +
    +        <!-- AWS SDK KMS -->
    +        <dependency>
    +            <groupId>software.amazon.awssdk</groupId>
    +            <artifactId>kms</artifactId>
    +            <version>${aws.sdk.version}</version>
    +        </dependency>
    +
    +        <!-- SAAJ implementation for MIME utilities -->
    +        <dependency>
    +            <groupId>com.sun.xml.messaging.saaj</groupId>
    +            <artifactId>saaj-impl</artifactId>
    +            <version>2.0.1</version>
    +        </dependency>
    +
    +        <!-- JUnit for testing -->
    +        <dependency>
    +            <groupId>org.junit.jupiter</groupId>
    +            <artifactId>junit-jupiter</artifactId>
    +            <version>5.10.0</version>
    +            <scope>test</scope>
    +        </dependency>
    +    </dependencies>
    +
    +    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-compiler-plugin</artifactId>
    +                <version>3.11.0</version>
    +                <configuration>
    +                    <source>8</source>
    +                    <target>8</target>
    +                </configuration>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.codehaus.mojo</groupId>
    +                <artifactId>exec-maven-plugin</artifactId>
    +                <version>3.1.0</version>
    +                <executions>
    +                    <execution>
    +                        <id>step1</id>
    +                        <configuration>
    +                            <mainClass>software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep1</mainClass>
    +                        </configuration>
    +                    </execution>
    +                    <execution>
    +                        <id>step2</id>
    +                        <configuration>
    +                            <mainClass>software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep2</mainClass>
    +                        </configuration>
    +                    </execution>
    +                    <execution>
    +                        <id>step3</id>
    +                        <configuration>
    +                            <mainClass>software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep3</mainClass>
    +                        </configuration>
    +                    </execution>
    +                </executions>
    +                <configuration>
    +                    <mainClass>software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep1</mainClass>
    +                </configuration>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-surefire-plugin</artifactId>
    +                <version>3.0.0</version>
    +            </plugin>
    +
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-shade-plugin</artifactId>
    +                <version>3.5.1</version>
    +                <executions>
    +                    <execution>
    +                        <phase>package</phase>
    +                        <goals>
    +                            <goal>shade</goal>
    +                        </goals>
    +                        <configuration>
    +                            <transformers>
    +                                <transformer
    +                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
    +                                    <mainClass>software.amazon.encryption.s3.examples.migration.v4.V4MigrationStep1
    +                                    </mainClass>
    +                                </transformer>
    +                            </transformers>
    +                            <finalName>v4-migration-examples</finalName>
    +                        </configuration>
    +                    </execution>
    +                </executions>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</project>
    
  • migration_examples/v3-to-v4/v4/src/main/java/software/amazon/encryption/s3/examples/migration/v4/V4MigrationStep1.java+122 0 added
    @@ -0,0 +1,122 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v4;
    +
    +import software.amazon.awssdk.core.sync.RequestBody;
    +import software.amazon.awssdk.core.sync.ResponseTransformer;
    +import software.amazon.awssdk.regions.Region;
    +import software.amazon.awssdk.services.kms.KmsClient;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.GetObjectRequest;
    +import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    +import software.amazon.encryption.s3.CommitmentPolicy;
    +import software.amazon.encryption.s3.S3EncryptionClient;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    +import software.amazon.encryption.s3.materials.KmsKeyring;
    +
    +/**
    + * Migration Step 1: This example demonstrates how to start using the S3 Encryption Client v4.
    + * <p>
    + * This example's purpose is to demonstrate the code changes to
    + * migrate from the v3 client to the v4 client while maintaining identical behavior.
    + * <p>
    + * When starting from a v3 client modeled in "Migration Step 0",
    + * "Migration Step 1" should result in no behavioral changes to your application.
    + * <p>
    + * In this example we configure a v4 client to:
    + * - Write objects encrypted with non-key committing algorithms
    + * - Read objects encrypted either with or without key committing algorithms
    + * <p>
    + * In this configuration, the client will continue to read objects encrypted
    + * with non-key committing algorithms (written by a v3 client or this migration-in-progress v4 client),
    + * as well as objects encrypted by a migrated v4 client
    + * that is configured to write objects encrypted with key committing algorithms.
    + * <p>
    + * This configuration results in identical behavior to the S3 Encryption Client v3 client
    + * configured to use the default FORBID_ENCRYPT_ALLOW_DECRYPT commitment policy.
    + */
    +public class V4MigrationStep1 {
    +
    +    private static final int CURRENT_MIGRATION_STEP = 1;
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region) {
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region, CURRENT_MIGRATION_STEP);
    +    }
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region,
    +                                           int sourceStep) {
    +        // Test data for encryption
    +        String testData = "Hello, World! This is a test message for S3 encryption client migration.";
    +
    +        // Create regular S3 client
    +        S3Client s3Client = S3Client.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS client
    +        KmsClient kmsClient = KmsClient.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS keyring
    +        KmsKeyring keyring = KmsKeyring.builder()
    +                .kmsClient(kmsClient)
    +                .wrappingKeyId(kmsKeyId)
    +                .build();
    +
    +        // Create S3 Encryption Client v4 with FORBID_ENCRYPT_ALLOW_DECRYPT commitment policy
    +        // Migration note: This is now the v4 client API
    +        // This MUST be explicitly configured to FORBID_ENCRYPT_ALLOW_DECRYPT.
    +        // While FORBID_ENCRYPT_ALLOW_DECRYPT is the default for v3 clients,
    +        // v4 clients default to REQUIRE_ENCRYPT_REQUIRE_DECRYPT.
    +        // This configuration ensures identical behavior to a v3 client.
    +        S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4()
    +                .keyring(keyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .build();
    +
    +        // Create object keys for PUT and GET operations
    +        // PUT: Always use current step
    +        String putObjectKey = String.format("%s-step-%d", objectKey, CURRENT_MIGRATION_STEP);
    +        // GET: Use sourceStep (debug parameter to test cross-compatibility between steps; defaults to 1)
    +        String getObjectKey = String.format("%s-step-%d", objectKey, sourceStep);
    +
    +        // Upload encrypted object using S3 Encryption Client
    +        encryptionClient.putObject(
    +                PutObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(putObjectKey)
    +                        .build(),
    +                RequestBody.fromString(testData));
    +
    +        // Download and decrypt object using S3 Encryption Client
    +        String decryptedData = encryptionClient.getObject(
    +                GetObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(getObjectKey)
    +                        .build(),
    +                ResponseTransformer.toBytes()
    +        ).asUtf8String();
    +
    +        // Verify the roundtrip was successful
    +        if (!decryptedData.equals(testData)) {
    +            throw new AssertionError(
    +                    String.format("Roundtrip failed - data mismatch. Original: %s, Decrypted: %s",
    +                            testData, decryptedData));
    +        }
    +
    +        // Clean up resources
    +        encryptionClient.close();
    +        kmsClient.close();
    +    }
    +
    +    public static void main(String[] args) throws Exception {
    +        String bucketName = args[0];
    +        String objectKey = args[1];
    +        String kmsKeyId = args[2];
    +        String region = args[3];
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region);
    +    }
    +}
    
  • migration_examples/v3-to-v4/v4/src/main/java/software/amazon/encryption/s3/examples/migration/v4/V4MigrationStep2.java+128 0 added
    @@ -0,0 +1,128 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v4;
    +
    +import software.amazon.awssdk.core.sync.RequestBody;
    +import software.amazon.awssdk.core.sync.ResponseTransformer;
    +import software.amazon.awssdk.regions.Region;
    +import software.amazon.awssdk.services.kms.KmsClient;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.GetObjectRequest;
    +import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    +import software.amazon.encryption.s3.CommitmentPolicy;
    +import software.amazon.encryption.s3.S3EncryptionClient;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    +import software.amazon.encryption.s3.materials.KmsKeyring;
    +
    +/**
    + * Migration Step 2: This example demonstrates how to update your v4 client configuration
    + * to start writing objects encrypted with key committing algorithms.
    + * <p>
    + * This example's purpose is to demonstrate the commitment policy code changes required to
    + * start writing objects encrypted with key committing algorithms
    + * and document the behavioral changes that will result from this change.
    + * <p>
    + * When starting from a v4 client modeled in "Migration Step 1",
    + * "Migration Step 2" WILL result in behavioral changes to your application.
    + * The client will start writing objects encrypted with key committing algorithms.
    + * <p>
    + * IMPORTANT: You MUST have updated your readers to be able to read objects encrypted with key committing algorithms
    + * before deploying the changes in this step.
    + * This means deploying the changes from either "Migration Step 0" (if readers are v3 clients)
    + * or "Migration Step 1" (if readers are v4 clients) to all of your readers
    + * before deploying the changes to "Migration Step 2".
    + * <p>
    + * Once you deploy this change to your writers, your readers will start seeing
    + * some objects encrypted with non-key committing algorithms,
    + * and some objects encrypted with key committing algorithms.
    + * Because the changes would have already been deployed to all readers from earlier migration steps,
    + * we can be sure that our entire system is ready to read both types of objects.
    + * After deploying these changes but before proceeding to "Migration Step 3",
    + * you MUST take extra steps to ensure that your system is no longer reading
    + * objects encrypted with non-key committing algorithms
    + * (such as re-encrypting any existing objects using key committing algorithms).
    + */
    +public class V4MigrationStep2 {
    +
    +    private static final int CURRENT_MIGRATION_STEP = 2;
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region) {
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region, CURRENT_MIGRATION_STEP);
    +    }
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region,
    +                                           int sourceStep) {
    +        // Test data for encryption
    +        String testData = "Hello, World! This is a test message for S3 encryption client migration.";
    +
    +        // Create regular S3 client
    +        S3Client s3Client = S3Client.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS client
    +        KmsClient kmsClient = KmsClient.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS keyring
    +        KmsKeyring keyring = KmsKeyring.builder()
    +                .kmsClient(kmsClient)
    +                .wrappingKeyId(kmsKeyId)
    +                .build();
    +
    +        // Create S3 Encryption Client v4 with REQUIRE_ENCRYPT_ALLOW_DECRYPT commitment policy
    +        // Migration note: The commitment policy has been updated to REQUIRE_ENCRYPT_ALLOW_DECRYPT.
    +        // This change causes the client to start writing objects encrypted with key committing algorithms.
    +        // The client will continue to be able to read objects encrypted with either
    +        // key committing or non-key committing algorithms.
    +        S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4()
    +                .keyring(keyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .build();
    +
    +        // Create object keys for PUT and GET operations
    +        // PUT: Always use current step
    +        String putObjectKey = String.format("%s-step-%d", objectKey, CURRENT_MIGRATION_STEP);
    +        // GET: Use sourceStep (debug parameter to test cross-compatibility between steps; defaults to 2)
    +        String getObjectKey = String.format("%s-step-%d", objectKey, sourceStep);
    +
    +        // Upload encrypted object using S3 Encryption Client
    +        encryptionClient.putObject(
    +                PutObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(putObjectKey)
    +                        .build(),
    +                RequestBody.fromString(testData));
    +
    +        // Download and decrypt object using S3 Encryption Client
    +        String decryptedData = encryptionClient.getObject(
    +                GetObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(getObjectKey)
    +                        .build(),
    +                ResponseTransformer.toBytes()
    +        ).asUtf8String();
    +
    +        // Verify the roundtrip was successful
    +        if (!decryptedData.equals(testData)) {
    +            throw new AssertionError(
    +                    String.format("Roundtrip failed - data mismatch. Original: %s, Decrypted: %s",
    +                            testData, decryptedData));
    +        }
    +
    +        // Clean up resources
    +        encryptionClient.close();
    +        kmsClient.close();
    +    }
    +
    +    public static void main(String[] args) throws Exception {
    +        String bucketName = args[0];
    +        String objectKey = args[1];
    +        String kmsKeyId = args[2];
    +        String region = args[3];
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region);
    +    }
    +}
    
  • migration_examples/v3-to-v4/v4/src/main/java/software/amazon/encryption/s3/examples/migration/v4/V4MigrationStep3.java+126 0 added
    @@ -0,0 +1,126 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v4;
    +
    +import software.amazon.awssdk.core.sync.RequestBody;
    +import software.amazon.awssdk.core.sync.ResponseTransformer;
    +import software.amazon.awssdk.regions.Region;
    +import software.amazon.awssdk.services.kms.KmsClient;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.GetObjectRequest;
    +import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    +import software.amazon.encryption.s3.CommitmentPolicy;
    +import software.amazon.encryption.s3.S3EncryptionClient;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    +import software.amazon.encryption.s3.materials.KmsKeyring;
    +
    +/**
    + * Migration Step 3: This example demonstrates how to update your v4 client configuration
    + * to stop reading objects encrypted with non-key committing algorithms.
    + * <p>
    + * This example's purpose is to demonstrate the commitment policy code changes required to
    + * stop reading objects encrypted with non-key committing algorithms
    + * and document the behavioral changes that will result from this change.
    + * <p>
    + * When starting from a v4 client modeled in "Migration Step 2",
    + * "Migration Step 3" WILL result in behavioral changes to your application.
    + * The client will no longer be able to read objects encrypted with non-key committing algorithms.
    + * Before deploying these changes, you MUST have taken some extra steps
    + * to ensure that your system is no longer reading such objects,
    + * such as re-encrypting them with key committing algorithms.
    + * <p>
    + * IMPORTANT: Before deploying the changes in this step, your system should not be reading
    + * any objects encrypted with non-key committing algorithms.
    + * The changes in this step will cause such read attempts to fail.
    + * This means the changes from "Migration Step 2" should have already been deployed to all of your readers
    + * before you deploy the changes from "Migration Step 3".
    + * <p>
    + * Once you complete Step 3, you can be sure that all items being read by your system
    + * have been encrypted using key committing algorithms.
    + */
    +public class V4MigrationStep3 {
    +
    +    private static final int CURRENT_MIGRATION_STEP = 3;
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region) {
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region, CURRENT_MIGRATION_STEP);
    +    }
    +
    +    public static void runMigrationExample(String bucketName, String objectKey, String kmsKeyId, String region,
    +                                           int sourceStep) {
    +        // Test data for encryption
    +        String testData = "Hello, World! This is a test message for S3 encryption client migration.";
    +
    +        // Create regular S3 client
    +        S3Client s3Client = S3Client.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS client
    +        KmsClient kmsClient = KmsClient.builder()
    +                .region(Region.of(region))
    +                .build();
    +
    +        // Create KMS keyring
    +        KmsKeyring keyring = KmsKeyring.builder()
    +                .kmsClient(kmsClient)
    +                .wrappingKeyId(kmsKeyId)
    +                .build();
    +
    +        // Create S3 Encryption Client v4 with REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy
    +        // Migration note: The commitment policy has been changed to REQUIRE_ENCRYPT_REQUIRE_DECRYPT.
    +        // This change causes the client to stop reading objects encrypted without key committing algorithms.
    +        // IMPORTANT: Ensure your system is no longer reading such objects before deploying this change.
    +        // REQUIRE_ENCRYPT_REQUIRE_DECRYPT is also the default commitment policy for v4 clients,
    +        // so you do not need to set this explicitly (this is the same as Step 2, but with confirmation
    +        // that no legacy objects are being read).
    +        S3EncryptionClient encryptionClient = S3EncryptionClient.builderV4()
    +                .keyring(keyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .build();
    +
    +        // Create object keys for PUT and GET operations
    +        // PUT: Always use current step
    +        String putObjectKey = String.format("%s-step-%d", objectKey, CURRENT_MIGRATION_STEP);
    +        // GET: Use sourceStep (debug parameter to test cross-compatibility between steps; defaults to 3)
    +        String getObjectKey = String.format("%s-step-%d", objectKey, sourceStep);
    +
    +        // Upload encrypted object using S3 Encryption Client
    +        encryptionClient.putObject(
    +                PutObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(putObjectKey)
    +                        .build(),
    +                RequestBody.fromString(testData));
    +
    +        // Download and decrypt object using S3 Encryption Client
    +        String decryptedData = encryptionClient.getObject(
    +                GetObjectRequest.builder()
    +                        .bucket(bucketName)
    +                        .key(getObjectKey)
    +                        .build(),
    +                ResponseTransformer.toBytes()
    +        ).asUtf8String();
    +
    +        // Verify the roundtrip was successful
    +        if (!decryptedData.equals(testData)) {
    +            throw new AssertionError(
    +                    String.format("Roundtrip failed - data mismatch. Original: %s, Decrypted: %s",
    +                            testData, decryptedData));
    +        }
    +
    +        // Clean up resources
    +        encryptionClient.close();
    +        kmsClient.close();
    +    }
    +
    +    public static void main(String[] args) throws Exception {
    +        String bucketName = args[0];
    +        String objectKey = args[1];
    +        String kmsKeyId = args[2];
    +        String region = args[3];
    +        runMigrationExample(bucketName, objectKey, kmsKeyId, region);
    +    }
    +}
    
  • migration_examples/v3-to-v4/v4/src/test/java/software/amazon/encryption/s3/examples/migration/v4/V4MigrationStepsTest.java+104 0 added
    @@ -0,0 +1,104 @@
    +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package software.amazon.encryption.s3.examples.migration.v4;
    +
    +import org.junit.jupiter.api.AfterAll;
    +import org.junit.jupiter.api.Test;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
    +import software.amazon.awssdk.regions.Region;
    +import software.amazon.encryption.s3.S3EncryptionClientException;
    +
    +import static org.junit.jupiter.api.Assertions.assertThrows;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
    +
    +/**
    + * Tests for V4 Migration Step examples.
    + * These tests require AWS credentials and resources to be configured via environment variables.
    + */
    +public class V4MigrationStepsTest {
    +
    +    private static final String BUCKET_NAME = System.getenv("AWS_S3EC_TEST_BUCKET");
    +    private static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID");
    +    private static final String REGION = System.getenv("AWS_REGION");
    +    private static final String TEST_OBJECT_KEY = "migration-test-v4-" + ((int) (Math.random() * 100000));
    +
    +    @Test
    +    public void testV4MigrationStep1() {
    +        // Successfully executes step 1
    +        // Step 1 writes without key commitment
    +        V4MigrationStep1.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 1);
    +
    +        // Given: Step 2 has succeeded (writes with key commitment)
    +        V4MigrationStep2.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 2);
    +
    +        // When: Execute Step 1 with sortReadValue=2, Then: Success (i.e. can read values written with key commitment)
    +        V4MigrationStep1.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 2);
    +
    +        // Given: Step 3 has succeeded (writes with key commitment)
    +        V4MigrationStep3.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 3);
    +
    +        // When: Execute Step 1 with sortReadValue=3, Then: Success (i.e. can read values written with key commitment)
    +        V4MigrationStep1.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 3);
    +    }
    +
    +    @Test
    +    public void testV4MigrationStep2() {
    +        // Successfully executes step 2
    +        // Step 2 writes with key commitment
    +        V4MigrationStep2.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 2);
    +
    +        // Given: Step 1 has succeeded (writes without key commitment)
    +        V4MigrationStep1.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 1);
    +
    +        // When: Execute Step 2 with sortReadValue=1, Then: Success (i.e. can read values written without key commitment)
    +        V4MigrationStep2.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 1);
    +
    +        // Given: Step 3 has succeeded (writes with key commitment)
    +        V4MigrationStep3.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 3);
    +
    +        // When: Execute Step 2 with sortReadValue=3, Then: Success (i.e. can read values written with key commitment)
    +        V4MigrationStep2.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 3);
    +    }
    +
    +    @Test
    +    public void testV4MigrationStep3() {
    +        // Successfully executes step 3
    +        // Step 3 writes with key commitment
    +        V4MigrationStep3.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 3);
    +
    +        // Given: Step 1 has succeeded (writes without key commitment)
    +        V4MigrationStep1.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 1);
    +
    +        // When: Execute Step 3 with sortReadValue=1, Then: Fails with commitment policy violation
    +        // (i.e. cannot read values written without key commitment)
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
    +            V4MigrationStep3.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 1)
    +        );
    +        assertTrue(exception.getMessage().contains("Commitment policy violation"),
    +                "Expected commitment policy violation message, but got: " + exception.getMessage());
    +
    +        // Given: Step 2 has succeeded (writes with key commitment)
    +        V4MigrationStep2.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 2);
    +
    +        // When: Execute Step 3 with sortReadValue=2, Then: Success (i.e. can read values written with key commitment)
    +        V4MigrationStep3.runMigrationExample(BUCKET_NAME, TEST_OBJECT_KEY, KMS_KEY_ID, REGION, 2);
    +    }
    +
    +    @AfterAll
    +    public static void cleanupTestObjects() {
    +        try (S3Client s3Client = S3Client.builder()
    +                .region(Region.of(REGION))
    +                .build()) {
    +            // Delete objects created by each migration step
    +            for (int step = 1; step <= 3; step++) {
    +                String objectKey = String.format("%s-step-%d", TEST_OBJECT_KEY, step);
    +                s3Client.deleteObject(DeleteObjectRequest.builder()
    +                        .bucket(BUCKET_NAME)
    +                        .key(objectKey)
    +                        .build());
    +            }
    +        }
    +    }
    +}
    
  • README.md+49 14 modified
    @@ -53,7 +53,36 @@ You can configure each client independently, or apply a "top-level" configuratio
     Refer to the Client Configuration Example in the [Examples directory](https://github.com/aws/amazon-s3-encryption-client-java/tree/main/src/examples/java/software/amazon/encryption/s3/examples) for examples of each configuration method.
     
     ### Examples
    -#### V2 KMS Materials Provider to V3
    +#### V3 to V4 Migration
    +
    +V4 introduces enhanced security with commitment policies. To migrate from V3:
    +
    +```java
    +class Example {
    +    public static void main(String[] args) {
    +        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    +        keyGen.init(256);
    +        SecretKey aesKey = keyGen.generateKey();
    +        
    +        // V3
    +        S3Client v3Client = S3EncryptionClient.builder()
    +                .aesKey(aesKey)
    +                .build();
    +        
    +        // V4
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .aesKey(aesKey)
    +                .build();
    +    }
    +}
    +```
    +
    +For detailed migration guidance and step-by-step examples, refer to the [Migration Examples](migration_examples/v3-to-v4/README.md). For more information, refer to the <a href="https://docs.aws.amazon.com/amazon-s3-encryption-client/latest/developerguide/java-v4-migration.html">Developer Guide</a>.
    +
    +
    +#### V2 KMS Materials Provider to V4
     ```java
     class Example {
         public static void main(String[] args) {
    @@ -63,15 +92,17 @@ class Example {
                     .withEncryptionMaterialsProvider(materialsProvider)
                     .build();
             
    -        // V3
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        // V4
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_WRAPPING_KEY_ID)
                     .build();
         }
     }
     ```
     
    -#### V2 AES Key Materials Provider to V3
    +#### V2 AES Key Materials Provider to V4
     ```java
     class Example {
         public static void main(String[] args) {
    @@ -85,15 +116,17 @@ class Example {
                     .withEncryptionMaterialsProvider(materialsProvider)
                     .build();
     
    -        // V3
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        // V4
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .build();
         }
     }
     ```
     
    -#### V2 RSA Key Materials Provider to V3
    +#### V2 RSA Key Materials Provider to V4
     ```java
     class Example {
         public static void main(String[] args) {
    @@ -107,15 +140,17 @@ class Example {
                     .withEncryptionMaterialsProvider(materialsProvider)
                     .build();
     
    -        // V3
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        // V4
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(rsaKey)
                     .build();
         }
     }
     ```
     
    -#### V1 Key Materials Provider to V3
    +#### V1 Key Materials Provider to V4
     To allow legacy modes (for decryption only), you must explicitly allow them
     ```java
     class Example {
    @@ -130,8 +165,10 @@ class Example {
                     .withEncryptionMaterials(materialsProvider)
                     .build();
     
    -        // V3
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        // V4
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .enableLegacyUnauthenticatedModes(true) // for enabling legacy content decryption modes
                     .enableLegacyWrappingAlgorithms(true) // for enabling legacy key wrapping modes 
    @@ -149,8 +186,6 @@ class Example {
     * RSA-OAEP w/MGF-1 and SHA-256
     * RSA
     * KMS (without context)
    -#### Encryption Metadata Storage
    -* Instruction File
     
     ## Security
     
    
  • release-prepare.sh+27 0 added
    @@ -0,0 +1,27 @@
    +#!/bin/bash
    +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
    +# SPDX-License-Identifier: Apache-2.0
    +
    +set -e
    +
    +VERSION=$1
    +MAJOR_VERSION=$(echo "$VERSION" | cut -d. -f1)
    +
    +echo "Preparing release for version $VERSION (major: $MAJOR_VERSION)"
    +
    +# Update Maven versions
    +mvn versions:set -DnewVersion="$VERSION" -DautoVersionSubmodules=true
    +
    +# Update s3ec version in migration examples
    +sed -i '' "s/<s3ec.version>.*<\/s3ec.version>/<s3ec.version>$VERSION<\/s3ec.version>/g" migration_examples/v3-to-v4/v4/pom.xml
    +
    +# Update API_VERSION_UNKNOWN with major version
    +sed -i '' "s/public static final String API_VERSION_UNKNOWN = \".*-unknown\"/public static final String API_VERSION_UNKNOWN = \"$MAJOR_VERSION-unknown\"/g" src/main/java/software/amazon/encryption/s3/internal/ApiNameVersion.java
    +
    +# Update EXPECTED_API_MAJOR_VERSION
    +sed -i '' "s/EXPECTED_API_MAJOR_VERSION = \"[0-9]*\"/EXPECTED_API_MAJOR_VERSION = \"$MAJOR_VERSION\"/g" src/test/java/software/amazon/encryption/s3/internal/ApiNameVersionTest.java
    +
    +# Update version in README
    +sed -i '' "s/<version>.*<\/version>/<version>$VERSION<\/version>/g" README.md
    +
    +echo "Release preparation complete"
    
  • .releaserc+79 59 modified
    @@ -1,64 +1,84 @@
     ## Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
     ## SPDX-License-Identifier: Apache-2.0
     {
    -    "branches": ["main"],
    -    "plugins": [
    -          ["@semantic-release/commit-analyzer", {
    -            "preset": "conventionalcommits",
    -            "parserOpts": {
    -                "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
    -            },
    -            "presetConfig": {
    -                "types": [
    -                    {"type": "feat", "section": "Features"},
    -                    {"type": "fix", "section": "Fixes"},
    -                    {"type": "chore", "section": "Maintenance"},
    -                    {"type": "docs", "section": "Maintenance"},
    -                    {"type": "revert", "section": "Fixes"},
    -                    {"type": "style", "hidden": true},
    -                    {"type": "refactor", "hidden": true},
    -                    {"type": "perf", "hidden": true},
    -                    {"type": "test", "hidden": true}
    -                ]
    -            },
    -            "releaseRules": [
    -                {"type": "docs", "release": "patch"},
    -                {"type": "revert", "release": "patch"},
    -                {"type": "chore", "release": "patch"}
    -            ]
    -          }],
    -          ["@semantic-release/release-notes-generator", {
    -            "preset": "conventionalcommits",
    -            "parserOpts": {
    -                "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
    -            },
    -            "presetConfig": {
    -                "types": [
    -                    {"type": "feat", "section": "Features"},
    -                    {"type": "fix", "section": "Fixes"},
    -                    {"type": "chore", "section": "Maintenance"},
    -                    {"type": "docs", "section": "Maintenance"},
    -                    {"type": "revert", "section": "Fixes"},
    -                    {"type": "style", "hidden": true},
    -                    {"type": "refactor", "hidden": true},
    -                    {"type": "perf", "hidden": true},
    -                    {"type": "test", "hidden": true}
    -                ]
    -            }
    -          }],
    -          ["@semantic-release/changelog", {
    -            "changelogFile": "./CHANGELOG.md",
    -            "changelogTitle": "# Changelog"
    -          }],
    -          ["@semantic-release/exec", {
    -            "prepareCmd": "mvn versions:set -DnewVersion=${nextRelease.version} \
    -                    -DautoVersionSubmodules=true && find README.md -type f \
    -                    -exec sed -i 's/<version>.*<\\/version>/<version>${nextRelease.version}<\\/version>/g' {} \\;"
    -          }],
    -          ["@semantic-release/git", {
    -            "assets": ["./CHANGELOG.md", "./pom.xml", "./README.md"],
    -            "message": "Amazon S3 Encryption Client ${nextRelease.version} Release -- ${new Date().toISOString().slice(0, 10)} \n\n${nextRelease.notes}"
    -          }],
    +  "branches": ["main"],
    +  "plugins": [
    +    [
    +      "@semantic-release/commit-analyzer",
    +      {
    +        "preset": "conventionalcommits",
    +        "parserOpts": {
    +          "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
    +        },
    +        "presetConfig": {
    +          "types": [
    +            {"type": "feat", "section": "Features"},
    +            {"type": "fix", "section": "Fixes"},
    +            {"type": "chore", "section": "Maintenance"},
    +            {"type": "docs", "section": "Maintenance"},
    +            {"type": "revert", "section": "Fixes"},
    +            {"type": "style", "hidden": true},
    +            {"type": "refactor", "hidden": true},
    +            {"type": "perf", "hidden": true},
    +            {"type": "test", "hidden": true}
    +          ]
    +        },
    +        "releaseRules": [
    +          {"type": "docs", "release": "patch"},
    +          {"type": "revert", "release": "patch"},
    +          {"type": "chore", "release": "patch"}
    +        ]
    +      }
         ],
    -    "repositoryUrl": "https://github.com/aws/amazon-s3-encryption-client-java",
    +    [
    +      "@semantic-release/release-notes-generator",
    +      {
    +        "preset": "conventionalcommits",
    +        "parserOpts": {
    +          "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
    +        },
    +        "presetConfig": {
    +          "types": [
    +            {"type": "feat", "section": "Features"},
    +            {"type": "fix", "section": "Fixes"},
    +            {"type": "chore", "section": "Maintenance"},
    +            {"type": "docs", "section": "Maintenance"},
    +            {"type": "revert", "section": "Fixes"},
    +            {"type": "style", "hidden": true},
    +            {"type": "refactor", "hidden": true},
    +            {"type": "perf", "hidden": true},
    +            {"type": "test", "hidden": true}
    +          ]
    +        }
    +      }
    +    ],
    +    [
    +      "@semantic-release/changelog",
    +      {
    +        "changelogFile": "./CHANGELOG.md",
    +        "changelogTitle": "# Changelog"
    +      }
    +    ],
    +    [
    +      "@semantic-release/exec",
    +      {
    +        "prepareCmd": "bash ./release-prepare.sh ${nextRelease.version}"
    +      }
    +    ],
    +    [
    +      "@semantic-release/git",
    +      {
    +        "assets": [
    +          "./CHANGELOG.md",
    +          "./pom.xml",
    +          "./README.md",
    +          "./migration_examples/v3-to-v4/v4/pom.xml",
    +          "./src/main/java/software/amazon/encryption/s3/internal/ApiNameVersion.java",
    +          "./src/test/java/software/amazon/encryption/s3/internal/ApiNameVersionTest.java"
    +        ],
    +        "message": "Amazon S3 Encryption Client ${nextRelease.version} Release -- ${new Date().toISOString().slice(0, 10)} \n\n${nextRelease.notes}"
    +      }
    +    ]
    +  ],
    +  "repositoryUrl": "https://github.com/aws/aws-s3-encryption-client-java"
     }
    
  • src/examples/java/software/amazon/encryption/s3/examples/AsyncClientExample.java+7 7 modified
    @@ -37,20 +37,20 @@ public static void AsyncClient(String bucket) {
             //
             // This means that the S3 Async Encryption Client can perform both encrypt and decrypt operations
             // as part of the S3 putObject and getObject operations.
    -        S3AsyncClient v3AsyncClient = S3AsyncEncryptionClient.builder()
    +        S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Call putObject to encrypt the object and upload it to S3
    -        CompletableFuture<PutObjectResponse> futurePut = v3AsyncClient.putObject(builder -> builder
    +        CompletableFuture<PutObjectResponse> futurePut = s3AsyncClient.putObject(builder -> builder
                     .bucket(bucket)
                     .key(OBJECT_KEY)
                     .build(), AsyncRequestBody.fromString(input));
             // Block on completion of the futurePut
             futurePut.join();
     
             // Call getObject to retrieve and decrypt the object from S3
    -        CompletableFuture<ResponseBytes<GetObjectResponse>> futureGet = v3AsyncClient.getObject(builder -> builder
    +        CompletableFuture<ResponseBytes<GetObjectResponse>> futureGet = s3AsyncClient.getObject(builder -> builder
                     .bucket(bucket)
                     .key(OBJECT_KEY)
                     .build(), AsyncResponseTransformer.toBytes());
    @@ -61,20 +61,20 @@ public static void AsyncClient(String bucket) {
             assertEquals(input, getResponse.asUtf8String());
     
             // Close the client
    -        v3AsyncClient.close();
    +        s3AsyncClient.close();
         }
     
         private static void cleanup(String bucket) {
             // Instantiate the client to delete object
    -        S3AsyncClient v3Client = S3AsyncEncryptionClient.builder()
    +        S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Call deleteObject to delete the object from given S3 Bucket
    -        v3Client.deleteObject(builder -> builder.bucket(bucket)
    +        s3Client.deleteObject(builder -> builder.bucket(bucket)
                     .key(OBJECT_KEY)).join();
     
             // Close the client
    -        v3Client.close();
    +        s3Client.close();
         }
     }
    
  • src/examples/java/software/amazon/encryption/s3/examples/ClientConfigurationExample.java+4 4 modified
    @@ -74,7 +74,7 @@ public static void CustomClientConfiguration() {
                 .build();
     
         // Instantiate the S3 Encryption Client using the configured clients and keyring.
    -    final S3Client s3Client = S3EncryptionClient.builder()
    +    final S3Client s3Client = S3EncryptionClient.builderV4()
                 .wrappedClient(wrappedClient)
                 .wrappedAsyncClient(wrappedAsyncClient)
                 .keyring(kmsKeyring)
    @@ -118,7 +118,7 @@ public static void TopLevelClientConfiguration() {
         // NOTE: If you use both the "top-level" configuration AND
         // custom configuration such as the above example, the custom client
         // configuration will take precedence.
    -    final S3Client s3Client = S3EncryptionClient.builder()
    +    final S3Client s3Client = S3EncryptionClient.builderV4()
                 .credentialsProvider(creds)
                 .region(Region.of(KMS_REGION.toString()))
                 .kmsKeyId(ALTERNATE_KMS_KEY)
    @@ -179,7 +179,7 @@ public static void CustomClientConfigurationAsync() {
                 .build();
     
         // Instantiate the S3 Async Encryption Client using the configured clients and keyring.
    -    final S3AsyncClient s3Client = S3AsyncEncryptionClient.builder()
    +    final S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
                 .wrappedClient(wrappedAsyncClient)
                 .keyring(kmsKeyring)
                 .build();
    @@ -222,7 +222,7 @@ public static void TopLevelClientConfigurationAsync() {
         // NOTE: If you use both the "top-level" configuration AND
         // custom configuration such as the above example, the custom client
         // configuration will take precedence.
    -    final S3AsyncClient s3Client = S3AsyncEncryptionClient.builder()
    +    final S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
                 .credentialsProvider(creds)
                 .region(Region.of(KMS_REGION.toString()))
                 .kmsKeyId(ALTERNATE_KMS_KEY)
    
  • src/examples/java/software/amazon/encryption/s3/examples/MultipartUploadExample.java+13 13 modified
    @@ -59,13 +59,13 @@ public static void LowLevelMultipartUpload() throws IOException {
             //
             // This means that the S3 Encryption Client can perform both encrypt and decrypt operations
             // as part of the S3 putObject and getObject operations.
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .enableDelayedAuthenticationMode(true)
                     .build();
     
             // Create Multipart upload request to S3
    -        CreateMultipartUploadResponse initiateResult = v3Client.createMultipartUpload(builder ->
    +        CreateMultipartUploadResponse initiateResult = s3Client.createMultipartUpload(builder ->
                     builder.bucket(BUCKET).key(objectKey));
     
             List<CompletedPart> partETags = new ArrayList<>();
    @@ -96,7 +96,7 @@ public static void LowLevelMultipartUpload() throws IOException {
                 final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
     
                 // Upload all the different parts of the object
    -            UploadPartResponse uploadPartResult = v3Client.uploadPart(uploadPartRequest,
    +            UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
                         RequestBody.fromInputStream(partInputStream, partInputStream.available()));
     
                 // We need to add eTag's of all CompletedParts before calling CompleteMultipartUpload.
    @@ -124,7 +124,7 @@ public static void LowLevelMultipartUpload() throws IOException {
     
             // Upload the last part multipart upload to invoke `cipher.doFinal()`
             final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    -        UploadPartResponse uploadPartResult = v3Client.uploadPart(uploadPartRequest,
    +        UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
                     RequestBody.fromInputStream(partInputStream, partInputStream.available()));
     
             partETags.add(CompletedPart.builder()
    @@ -134,23 +134,23 @@ public static void LowLevelMultipartUpload() throws IOException {
     
             // Finally call completeMultipartUpload operation to tell S3 to merge all uploaded
             // parts and finish the multipart operation.
    -        v3Client.completeMultipartUpload(builder -> builder
    +        s3Client.completeMultipartUpload(builder -> builder
                     .bucket(BUCKET)
                     .key(objectKey)
                     .uploadId(initiateResult.uploadId())
                     .multipartUpload(partBuilder -> partBuilder.parts(partETags)));
     
             // Call getObject to retrieve and decrypt the object from S3
    -        ResponseInputStream<GetObjectResponse> output = v3Client.getObject(builder -> builder
    +        ResponseInputStream<GetObjectResponse> output = s3Client.getObject(builder -> builder
                     .bucket(BUCKET)
                     .key(objectKey));
     
             // Asserts
             assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
     
             // Cleanup
    -        v3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    -        v3Client.close();
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
         }
     
         /**
    @@ -175,7 +175,7 @@ public static void HighLevelMultipartPutObject() throws IOException {
             // This means that the S3 Encryption Client can perform both encrypt and decrypt operations
             // as part of the S3 putObject and getObject operations.
             // Note: You must also specify the `enableDelayedAuthenticationMode` parameter to perform getObject with more than 64MB object.
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .enableMultipartPutObject(true)
                     .enableDelayedAuthenticationMode(true)
    @@ -186,13 +186,13 @@ public static void HighLevelMultipartPutObject() throws IOException {
             encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
     
             // Call putObject to encrypt the object and upload it to S3.
    -        v3Client.putObject(builder -> builder
    +        s3Client.putObject(builder -> builder
                     .bucket(BUCKET)
                     .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
                     .key(objectKey), RequestBody.fromInputStream(inputStream, fileSizeLimit));
     
             // Call getObject to retrieve and decrypt the object from S3.
    -        ResponseInputStream<GetObjectResponse> output = v3Client.getObject(builder -> builder
    +        ResponseInputStream<GetObjectResponse> output = s3Client.getObject(builder -> builder
                     .bucket(BUCKET)
                     .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
                     .key(objectKey));
    @@ -201,7 +201,7 @@ public static void HighLevelMultipartPutObject() throws IOException {
             assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
     
             // Cleanup
    -        v3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    -        v3Client.close();
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
         }
     }
    
  • src/examples/java/software/amazon/encryption/s3/examples/PartialKeyPairExample.java+4 4 modified
    @@ -57,7 +57,7 @@ public static void useBothPublicAndPrivateKey(final String bucket) {
             // parameter.
             // This means that the S3 Encryption Client can perform both encrypt and decrypt operations
             // as part of the S3 putObject and getObject operations.
    -        S3Client s3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
    @@ -85,7 +85,7 @@ static void useOnlyPublicKey(final String bucket) {
             // public key from an RSA key pair with the PartialKeyPair object.
             // When you specify the public key alone, all GetObject calls will fail
             // because the private key is required to decrypt.
    -        S3Client s3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .rsaKeyPair(new PartialRsaKeyPair(null, RSA_KEY_PAIR.getPublic()))
                     .build();
     
    @@ -116,7 +116,7 @@ static void useOnlyPrivateKey(final String bucket) {
             // private key from an RSA key pair with the PartialRsaKeyPair object.
             // When you specify the private key alone, all PutObject calls will
             // fail because the public key is required to encrypt.
    -        S3Client s3ClientPrivateKeyOnly = S3EncryptionClient.builder()
    +        S3Client s3ClientPrivateKeyOnly = S3EncryptionClient.builderV4()
                     .rsaKeyPair(new PartialRsaKeyPair(RSA_KEY_PAIR.getPrivate(), null))
                     .build();
     
    @@ -135,7 +135,7 @@ static void useOnlyPrivateKey(final String bucket) {
             // Instantiate a new S3 Encryption client with a public key in order
             // to successfully call PutObject so that the client which only has
             // a private key can call GetObject on a valid S3 Object.
    -        S3Client s3ClientPublicKeyOnly = S3EncryptionClient.builder()
    +        S3Client s3ClientPublicKeyOnly = S3EncryptionClient.builderV4()
                     .rsaKeyPair(new PartialRsaKeyPair(null, RSA_KEY_PAIR.getPublic()))
                     .build();
     
    
  • src/examples/java/software/amazon/encryption/s3/examples/RangedGetExample.java+14 14 modified
    @@ -40,19 +40,19 @@ public static void simpleAesGcmV3RangedGet(String bucket) {
             //
             // This means that the S3 Encryption Client can perform both encrypt and decrypt operations,
             // and can perform ranged GET requests when a range is provided.
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .enableLegacyUnauthenticatedModes(true)
                     .build();
     
             // Call putObject to encrypt the object and upload it to S3
    -        v3Client.putObject(PutObjectRequest.builder()
    +        s3Client.putObject(PutObjectRequest.builder()
                                        .bucket(bucket)
                                        .key(objectKey)
                                        .build(), RequestBody.fromString(OBJECT_CONTENT));
     
             // Call getObject to retrieve a range of 10-20 bytes from the object content.
    -        ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        ResponseBytes<GetObjectResponse> objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("bytes=10-20")
                     .key(objectKey));
    @@ -63,8 +63,8 @@ public static void simpleAesGcmV3RangedGet(String bucket) {
             assertEquals(OBJECT_CONTENT.substring(10, 20 + 1), output);
     
             // Cleanup
    -        v3Client.deleteObject(builder -> builder.bucket(bucket).key(objectKey));
    -        v3Client.close();
    +        s3Client.deleteObject(builder -> builder.bucket(bucket).key(objectKey));
    +        s3Client.close();
         }
     
         /**
    @@ -75,20 +75,20 @@ public static void simpleAesGcmV3RangedGet(String bucket) {
         public static void aesGcmV3RangedGetOperations(String bucket) {
             final String objectKey = appendTestSuffix("aes-gcm-v3-ranged-get-examples");
     
    -        S3Client v3Client = S3EncryptionClient.builder()
    +        S3Client s3Client = S3EncryptionClient.builderV4()
                     .kmsKeyId(KMS_KEY_ID)
                     .enableLegacyUnauthenticatedModes(true)
                     .build();
     
             // Call putObject to encrypt the object and upload it to S3
    -        v3Client.putObject(PutObjectRequest.builder()
    +        s3Client.putObject(PutObjectRequest.builder()
                                        .bucket(bucket)
                                        .key(objectKey)
                                        .build(), RequestBody.fromString(OBJECT_CONTENT));
     
             // 1. Call getObject to retrieve a range of 190-300 bytes,
             // where 190 is within object range but 300 is outside the original plaintext object range.
    -        ResponseBytes<GetObjectResponse> objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        ResponseBytes<GetObjectResponse> objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("bytes=190-300")
                     .key(objectKey));
    @@ -100,7 +100,7 @@ public static void aesGcmV3RangedGetOperations(String bucket) {
     
             // 2. Call getObject to retrieve a range of 100-50 bytes,
             // where the start index is greater than the end index.
    -        objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("bytes=100-50")
                     .key(objectKey));
    @@ -111,7 +111,7 @@ public static void aesGcmV3RangedGetOperations(String bucket) {
             assertEquals(OBJECT_CONTENT, output);
     
             // 3. Call getObject to retrieve a range of 10-20 bytes but with invalid format
    -        objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("10-20")
                     .key(objectKey));
    @@ -124,7 +124,7 @@ public static void aesGcmV3RangedGetOperations(String bucket) {
     
             // 4. Call getObject to retrieve a range of 216-217 bytes.
             // Both the start and end indices are greater than the original plaintext object's total length, 200.
    -        objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("bytes=216-217")
                     .key(objectKey));
    @@ -136,7 +136,7 @@ public static void aesGcmV3RangedGetOperations(String bucket) {
     
             // 5. Call getObject to retrieve a range starting from byte 40 to the end of the object,
             // where the start index is within the object range, and the end index is unspecified.
    -        objectResponse = v3Client.getObjectAsBytes(builder -> builder
    +        objectResponse = s3Client.getObjectAsBytes(builder -> builder
                     .bucket(bucket)
                     .range("bytes=40-")
                     .key(objectKey));
    @@ -147,7 +147,7 @@ public static void aesGcmV3RangedGetOperations(String bucket) {
             assertEquals(OBJECT_CONTENT.substring(40), output);
     
             // Cleanup
    -        v3Client.deleteObject(builder -> builder.bucket(bucket).key(objectKey));
    -        v3Client.close();
    +        s3Client.deleteObject(builder -> builder.bucket(bucket).key(objectKey));
    +        s3Client.close();
         }
     }
    
  • src/examples/java/software/amazon/encryption/s3/examples/ReEncryptInstructionFileExample.java+6 6 modified
    @@ -102,7 +102,7 @@ public static void simpleAesKeyringReEncryptInstructionFile(
         // Create the S3 Encryption Client with instruction file support enabled
         // The client can perform both putObject and getObject operations using the original AES key
         S3EncryptionClient originalClient = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(oldKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    @@ -167,7 +167,7 @@ public static void simpleAesKeyringReEncryptInstructionFile(
     
         // Create a new client with the rotated AES key
         S3EncryptionClient newClient = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(newKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    @@ -241,7 +241,7 @@ public static void simpleRsaKeyringReEncryptInstructionFile(
         // Create the S3 Encryption Client with instruction file support enabled
         // The client can perform both putObject and getObject operations using RSA keyring
         S3EncryptionClient originalClient = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(originalKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    @@ -315,7 +315,7 @@ public static void simpleRsaKeyringReEncryptInstructionFile(
     
         // Create a new client with the rotated RSA key
         S3EncryptionClient newClient = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(newKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    @@ -395,7 +395,7 @@ public static void simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix(
         // Create the S3 Encryption Client with instruction file support enabled
         // The client can perform both putObject and getObject operations using RSA keyring
         S3EncryptionClient client = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(clientKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    @@ -472,7 +472,7 @@ public static void simpleRsaKeyringReEncryptInstructionFileWithCustomSuffix(
     
         // Create the third party's S3 Encryption Client
         S3EncryptionClient thirdPartyClient = S3EncryptionClient
    -      .builder()
    +      .builderV4()
           .keyring(thirdPartyKeyring)
           .instructionFileConfig(
             InstructionFileConfig
    
  • src/main/java/software/amazon/encryption/s3/internal/CipherProvider.java+35 24 modified
    @@ -17,6 +17,7 @@
     import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.materials.CryptographicMaterials;
     import software.amazon.encryption.s3.materials.DecryptionMaterials;
    +import software.amazon.encryption.s3.materials.EncryptionMaterials;
     
     /**
      * Composes a CMM to provide S3 specific functionality
    @@ -33,7 +34,7 @@ public class CipherProvider {
                 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01
         };
     
    -    public static SecretKey generateDerivedEncryptionKey(final DecryptionMaterials materials, byte[] messageId) {
    +    public static SecretKey generateDerivedEncryptionKey(final CryptographicMaterials materials, byte[] messageId) {
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
             //= type=implication
             //# - The hash function MUST be specified by the algorithm suite commitment settings.
    @@ -78,28 +79,27 @@ public static SecretKey generateDerivedEncryptionKey(final DecryptionMaterials m
     
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
             //# - The length of the output keying material MUST equal the commit key length specified by the supported algorithm suites.
    -        //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
    -        //= type=exception
    -        //# The derived key commitment value MUST be set or returned from the encryption process such that it can be included in the content metadata.
             final byte[] commitment = kdf.deriveKey(commitKeyLabel, materials.algorithmSuite().commitmentLengthBytes());
    -        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    -        //# When using an algorithm suite which supports key commitment, the client MUST verify the key commitment values match
    -        //# before deriving the [derived encryption key](./key-derivation.md#hkdf-operation).
    -        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    -        //# When using an algorithm suite which supports key commitment, the client MUST verify that the
    -        //# [derived key commitment](./key-derivation.md#hkdf-operation) contains the same bytes as the stored key
    -        //# commitment retrieved from the stored object's metadata.
    -        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    -        //= type=implication
    -        //# When using an algorithm suite which supports key commitment, the verification of the derived key commitment value
    -        //# MUST be done in constant time.
    -        if (!MessageDigest.isEqual(commitment, materials.getKeyCommitment())) {
    +        if (materials instanceof DecryptionMaterials) {
                 //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    -            //# When using an algorithm suite which supports key commitment, the client MUST throw an exception when the
    -            //# derived key commitment value and stored key commitment value do not match.
    -            throw new S3EncryptionClientSecurityException("Key commitment validation failed. " +
    -                    "The derived key commitment does not match the stored key commitment value. " +
    -                    "This indicates potential data tampering or corruption.");
    +            //# When using an algorithm suite which supports key commitment, the client MUST verify the key commitment values match
    +            //# before deriving the [derived encryption key](./key-derivation.md#hkdf-operation).
    +            //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +            //# When using an algorithm suite which supports key commitment, the client MUST verify that the
    +            //# [derived key commitment](./key-derivation.md#hkdf-operation) contains the same bytes as the stored key
    +            //# commitment retrieved from the stored object's metadata.
    +            //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +            //= type=implication
    +            //# When using an algorithm suite which supports key commitment, the verification of the derived key commitment value
    +            //# MUST be done in constant time.
    +            if (!MessageDigest.isEqual(commitment, materials.getKeyCommitment())) {
    +                //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +                //# When using an algorithm suite which supports key commitment, the client MUST throw an exception when the
    +                //# derived key commitment value and stored key commitment value do not match.
    +                throw new S3EncryptionClientSecurityException("Key commitment validation failed. " +
    +                        "The derived key commitment does not match the stored key commitment value. " +
    +                        "This indicates potential data tampering or corruption.");
    +            }
             }
     
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
    @@ -112,7 +112,18 @@ public static SecretKey generateDerivedEncryptionKey(final DecryptionMaterials m
     
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
             //# - The length of the output keying material MUST equal the encryption key length specified by the algorithm suite encryption settings.
    -        return new SecretKeySpec(kdf.deriveKey(deriveKeyLabel, materials.algorithmSuite().dataKeyLengthBytes()), materials.algorithmSuite().dataKeyAlgorithm());
    +        SecretKey ek =
    +                new SecretKeySpec(kdf.deriveKey(deriveKeyLabel, materials.algorithmSuite().dataKeyLengthBytes()), materials.algorithmSuite().dataKeyAlgorithm());
    +
    +        //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
    +        //# The derived key commitment value MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        if (materials instanceof EncryptionMaterials) {
    +            ((EncryptionMaterials) materials).setKeyCommitment(commitment);
    +        } else if (materials instanceof MultipartUploadMaterials) {
    +            ((MultipartUploadMaterials) materials).setKeyCommitment(commitment);
    +        }
    +
    +        return ek;
         }
     
         /**
    @@ -155,7 +166,7 @@ public static Cipher createAndInitCipher(final CryptographicMaterials materials,
                         //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
                         //# The client MUST use HKDF to derive the key commitment value and the derived encrypting key
                         //# as described in [Key Derivation](key-derivation.md).
    -                    actualKey = generateDerivedEncryptionKey((DecryptionMaterials)materials, messageId);
    +                    actualKey = generateDerivedEncryptionKey(materials, messageId);
                         //= specification/s3-encryption/key-derivation.md#hkdf-operation
                         //# The client MUST initialize the cipher, or call an AES-GCM encryption API, with the derived encryption key, an IV containing only bytes with the value 0x01,
                         //# and the tag length defined in the Algorithm Suite when encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.
    @@ -194,7 +205,7 @@ public static Cipher createAndInitCipher(final CryptographicMaterials materials,
                         //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
                         //# The client MUST use HKDF to derive the key commitment value and the derived encrypting key
                         //# as described in [Key Derivation](key-derivation.md).
    -                    actualKey = generateDerivedEncryptionKey((DecryptionMaterials) materials, messageId);
    +                    actualKey = generateDerivedEncryptionKey(materials, messageId);
                         cipher.init(materials.cipherMode().opMode(), actualKey, new IvParameterSpec(iv));
                         break;
     
    
  • src/main/java/software/amazon/encryption/s3/internal/ContentMetadataEncodingStrategy.java+76 6 modified
    @@ -38,8 +38,8 @@ public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv,
                 String metadataString;
                 Map<String, String> objectMetadata;
                 if (materials.algorithmSuite().isCommitting()) {
    -                // TODO: Should throw an exception for Commiting Alg
    -                throw new S3EncryptionClientException("This version of S3EC does not support encryption with committing algorithm suite: " + materials.algorithmSuite());
    +                metadataString = metadataToStringForV3InstructionFile(materials, iv);
    +                objectMetadata = addMetadataToMapV3InstructionFile(putObjectRequest.metadata(), materials, iv);
                 } else {
                     metadataString = metadataToStringForV1V2InstructionFile(materials, iv);
                     objectMetadata = putObjectRequest.metadata();
    @@ -68,8 +68,8 @@ public CreateMultipartUploadRequest encodeMetadata(EncryptionMaterials materials
                 String metadataString;
                 Map<String, String> objectMetadata;
                 if (materials.algorithmSuite().isCommitting()) {
    -                // TODO: Should throw an exception for Commiting Alg
    -                throw new S3EncryptionClientException("This version of S3EC does not support encryption with committing algorithm suite: " + materials.algorithmSuite());
    +                metadataString = metadataToStringForV3InstructionFile(materials, iv);
    +                objectMetadata = addMetadataToMapV3InstructionFile(createMultipartUploadRequest.metadata(), materials, iv);
                 } else {
                     metadataString = metadataToStringForV1V2InstructionFile(materials, iv);
                     objectMetadata = createMultipartUploadRequest.metadata();
    @@ -92,6 +92,19 @@ private String metadataToStringForV1V2InstructionFile(EncryptionMaterials materi
             final Map<String, String> metadataMap = addMetadataToMap(new HashMap<>(), materials, iv);
             return metadataToString(metadataMap);
         }
    +    private String metadataToStringForV3InstructionFile(EncryptionMaterials materials, byte[] iv) {
    +        final Map<String, String> metadataMap = addMetadataToMap(new HashMap<>(), materials, iv);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-c" and its value in the Instruction File.
    +        metadataMap.remove(MetadataKeyConstants.CONTENT_CIPHER_V3);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-d" and its value in the Instruction File.
    +        metadataMap.remove(MetadataKeyConstants.KEY_COMMITMENT_V3);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-i" and its value in the Instruction File.
    +        metadataMap.remove(MetadataKeyConstants.MESSAGE_ID_V3);
    +        return metadataToString(metadataMap);
    +    }
     
         private String metadataToString(Map<String, String> metadataMap) {
             try (JsonWriter jsonWriter = JsonWriter.create()) {
    @@ -107,10 +120,67 @@ private String metadataToString(Map<String, String> metadataMap) {
             }
         }
     
    +    private Map<String, String> addMetadataToMapV3InstructionFile(Map<String, String> map, EncryptionMaterials materials, byte[] iv) {
    +        Map<String, String> metadata = new HashMap<>(map);
    +        //= 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.
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST store the mapkey "x-amz-c" and its value in the Object Metadata when writing with an Instruction File.
    +        metadata.put(MetadataKeyConstants.CONTENT_CIPHER_V3, materials.algorithmSuite().idAsString());
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST store the mapkey "x-amz-d" and its value in the Object Metadata when writing with an Instruction File.
    +        metadata.put(MetadataKeyConstants.KEY_COMMITMENT_V3, ENCODER.encodeToString(materials.getKeyCommitment()));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST store the mapkey "x-amz-i" and its value in the Object Metadata when writing with an Instruction File.
    +        metadata.put(MetadataKeyConstants.MESSAGE_ID_V3, ENCODER.encodeToString(iv));
    +        return metadata;
    +    }
    +
    +    private Map<String, String> addMetadataToMapV3(Map<String, String> map, EncryptionMaterials materials, byte[] iv) {
    +        Map<String, String> metadata = new HashMap<>(map);
    +        EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST store the mapkey "x-amz-3" and its value in the Instruction File.
    +        metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3, ENCODER.encodeToString(edk.encryptedDatakey()));
    +        metadata.put(MetadataKeyConstants.MESSAGE_ID_V3, ENCODER.encodeToString(iv));
    +        metadata.put(MetadataKeyConstants.CONTENT_CIPHER_V3, materials.algorithmSuite().idAsString());
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //# - The V3 message format MUST store the mapkey "x-amz-w" and its value in the Instruction File.
    +        String keyProviderInfo = MetadataKeyConstants.compressWrappingAlgorithm(edk.keyProviderInfo());
    +        metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3, keyProviderInfo);
    +        metadata.put(MetadataKeyConstants.KEY_COMMITMENT_V3, ENCODER.encodeToString(materials.getKeyCommitment()));
    +
    +        try (JsonWriter jsonWriter = JsonWriter.create()) {
    +            jsonWriter.writeStartObject();
    +            if (!materials.encryptionContext().isEmpty() && materials.materialsDescription().isEmpty()) {
    +                // write EncryptionContext
    +                for (Map.Entry<String, String> entry : materials.encryptionContext().entrySet()) {
    +                    jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
    +                }
    +                jsonWriter.writeEndObject();
    +                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.
    +                metadata.put(MetadataKeyConstants.ENCRYPTION_CONTEXT_V3, jsonEncryptionContext);
    +            } else if (materials.encryptionContext().isEmpty() && !materials.materialsDescription().isEmpty()) {
    +                // write EncryptionContext
    +                for (Map.Entry<String, String> entry : materials.materialsDescription().entrySet()) {
    +                    jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
    +                }
    +                jsonWriter.writeEndObject();
    +                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.
    +                metadata.put(MetadataKeyConstants.MAT_DESC_V3, jsonEncryptionContext);
    +            }
    +        } catch (JsonWriter.JsonGenerationException e) {
    +            throw new S3EncryptionClientException("Cannot serialize encryption context or mat desc to JSON.", e);
    +        }
    +        return metadata;
    +    }
         private Map<String, String> addMetadataToMap(Map<String, String> map, EncryptionMaterials materials, byte[] iv) {
             if (materials.algorithmSuite().isCommitting()) {
    -            // TODO: Should throw an exception for Commiting Alg
    -            throw new S3EncryptionClientException("This version of S3EC does not support encryption with committing algorithm suite: " + materials.algorithmSuite());
    +            return addMetadataToMapV3(map, materials, iv);
             }
             Map<String, String> metadata = new HashMap<>(map);
             EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
    
  • src/main/java/software/amazon/encryption/s3/internal/GetEncryptedObjectPipeline.java+9 12 modified
    @@ -2,6 +2,14 @@
     // SPDX-License-Identifier: Apache-2.0
     package software.amazon.encryption.s3.internal;
     
    +import static software.amazon.encryption.s3.internal.ApiNameVersion.API_NAME_INTERCEPTOR;
    +
    +import java.nio.ByteBuffer;
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.concurrent.CompletableFuture;
    +
     import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import software.amazon.awssdk.core.async.AsyncResponseTransformer;
     import software.amazon.awssdk.core.async.SdkPublisher;
    @@ -18,14 +26,6 @@
     import software.amazon.encryption.s3.materials.DecryptionMaterials;
     import software.amazon.encryption.s3.materials.EncryptedDataKey;
     
    -import java.nio.ByteBuffer;
    -import java.util.Arrays;
    -import java.util.Collections;
    -import java.util.List;
    -import java.util.concurrent.CompletableFuture;
    -
    -import static software.amazon.encryption.s3.internal.ApiNameVersion.API_NAME_INTERCEPTOR;
    -
     /**
      * This class will determine the necessary mechanisms to decrypt objects returned from S3.
      * Due to supporting various legacy modes, this is not a predefined pipeline like
    @@ -100,11 +100,9 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
             }
     
             //= specification/s3-encryption/decryption.md#key-commitment
    -        //= type=implication
             //# The S3EC MUST validate the algorithm suite used for decryption against the key commitment policy before attempting to decrypt the content ciphertext.
             if (_commitmentPolicy.requiresDecrypt() && !algorithmSuite.isCommitting()) {
                 //= specification/s3-encryption/decryption.md#key-commitment
    -            //= type=implication
                 //# If the commitment policy requires decryption using a committing algorithm suite, and the algorithm suite
                 //# associated with the object does not support key commitment, then the S3EC MUST throw an exception.
                 throw new S3EncryptionClientException("Commitment policy violation, decryption requires a committing algorithm suite, " +
    @@ -122,6 +120,7 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
                     .materialsDescription(contentMetadata.materialsDescription())
                     .ciphertextLength(getObjectResponse.contentLength())
                     .contentRange(getObjectRequest.range())
    +                .keyCommitment(contentMetadata.keyCommitment())
                     .build();
     
             DecryptionMaterials materials = _cryptoMaterialsManager.decryptMaterials(materialsRequest);
    @@ -130,8 +129,6 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
                         "This may be caused by a misconfigured custom CMM implementation or " +
                         "a suppressed exception from metadata decoding or CMM invocation due to a network failure.");
             }
    -        materials.setKeyCommitment(contentMetadata.keyCommitment());
    -
             return materials;
         }
     
    
  • src/main/java/software/amazon/encryption/s3/internal/MultipartUploadMaterials.java+43 6 modified
    @@ -8,14 +8,17 @@
     import software.amazon.encryption.s3.S3EncryptionClientException;
     import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.materials.CryptographicMaterials;
    +import software.amazon.encryption.s3.materials.EncryptedDataKey;
     import software.amazon.encryption.s3.materials.EncryptionMaterials;
    +import software.amazon.encryption.s3.materials.MaterialsDescription;
     
     import javax.crypto.Cipher;
     import javax.crypto.SecretKey;
     import javax.crypto.spec.SecretKeySpec;
     import java.security.MessageDigest;
     import java.security.Provider;
     import java.util.Collections;
    +import java.util.List;
     import java.util.Map;
     
     /**
    @@ -40,6 +43,9 @@ public class MultipartUploadMaterials implements CryptographicMaterials {
         private long _plaintextLength;
         private boolean hasFinalPartBeenSeen;
         private final Cipher _cipher;
    +    private byte[] _keyCommitment;
    +    private byte[] _messageId;
    +    private byte[] _iv;
     
         private MultipartUploadMaterials(Builder builder) {
             this._s3Request = builder._s3Request;
    @@ -49,6 +55,7 @@ private MultipartUploadMaterials(Builder builder) {
             this._cryptoProvider = builder._cryptoProvider;
             this._plaintextLength = builder._plaintextLength;
             this._cipher = builder._cipher;
    +        this._keyCommitment = builder._keyCommitment;
         }
     
         static public Builder builder() {
    @@ -85,10 +92,6 @@ public Cipher getCipher(byte[] iv) {
             return _cipher;
         }
     
    -    public byte[] getIv() {
    -        return _cipher.getIV();
    -    }
    -
         /**
          * Can be used to check the next part number must either be the same (if it
          * was a retry) or increment by exactly 1 during a serial part uploads.
    @@ -125,6 +128,7 @@ protected void beginPartUpload(final int nextPartNumber, final long partContentL
         /**
          * Increments the plaintextSize as parts come in, checking to
          * ensure that the max GCM size limit is not exceeded.
    +     *
          * @param lengthOfPartToAdd the length of the incoming part
          * @return the new _plaintextLength value
          */
    @@ -184,14 +188,43 @@ public CipherMode cipherMode() {
             return CipherMode.MULTIPART_ENCRYPT;
         }
     
    +    @Override
    +    public byte[] getKeyCommitment() {
    +        return _keyCommitment != null ? _keyCommitment.clone() : null;
    +    }
    +
    +    @Override
    +    public byte[] messageId() {
    +        return _messageId != null ? _messageId.clone() : null;
    +    }
    +
    +    @Override
    +    public byte[] iv() {
    +        return _iv != null ? _iv.clone() : null;
    +    }
    +
    +    public void setKeyCommitment(byte[] keyCommitment) {
    +        _keyCommitment = keyCommitment;
    +    }
    +
    +    public void setIvAndMessageId(byte[] iv, byte[] messageId) {
    +        this._iv = iv;
    +        this._messageId = messageId;
    +    }
    +
         static public class Builder {
             private S3Request _s3Request = null;
    -        private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
    +        private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
             private Map<String, String> _encryptionContext = Collections.emptyMap();
             private byte[] _plaintextDataKey = null;
    -        private long _plaintextLength = 0;
    +        private final long _plaintextLength = 0;
             private Provider _cryptoProvider = null;
             private Cipher _cipher = null;
    +        private byte[] _keyCommitment = null;
    +        private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build();
    +        private List<EncryptedDataKey> _encryptedDataKeys;
    +        private byte[] _iv;
    +        private byte[] _messageId;
     
             private Builder() {
             }
    @@ -205,8 +238,12 @@ public Builder fromEncryptionMaterials(final EncryptionMaterials materials) {
                 _s3Request = materials.s3Request();
                 _algorithmSuite = materials.algorithmSuite();
                 _encryptionContext = materials.encryptionContext();
    +            _encryptedDataKeys = materials.encryptedDataKeys();
                 _plaintextDataKey = materials.plaintextDataKey();
                 _cryptoProvider = materials.cryptoProvider();
    +            _materialsDescription = materials.materialsDescription();
    +            _iv = materials.iv();
    +            _messageId = materials.messageId();
                 return this;
             }
     
    
  • src/main/java/software/amazon/encryption/s3/internal/MultipartUploadObjectPipeline.java+16 5 modified
    @@ -43,6 +43,8 @@ public class MultipartUploadObjectPipeline {
         final private CryptographicMaterialsManager _cryptoMaterialsManager;
         final private MultipartContentEncryptionStrategy _contentEncryptionStrategy;
         final private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy;
    +    final private InstructionFileConfig _instructionFileConfig;
    +    final private AlgorithmSuite _encryptionAlgorithm;
         /**
          * Map of data about in progress encrypted multipart uploads.
          */
    @@ -54,6 +56,8 @@ private MultipartUploadObjectPipeline(Builder builder) {
             this._contentEncryptionStrategy = builder._contentEncryptionStrategy;
             this._contentMetadataEncodingStrategy = builder._contentMetadataEncodingStrategy;
             this._multipartUploadMaterials = builder._multipartUploadMaterials;
    +        this._instructionFileConfig = builder._instructionFileConfig;
    +        this._encryptionAlgorithm = builder._encryptionAlgorithm;
         }
     
         public static Builder builder() {
    @@ -62,6 +66,7 @@ public static Builder builder() {
     
         public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) {
             EncryptionMaterialsRequest.Builder requestBuilder = EncryptionMaterialsRequest.builder()
    +                .encryptionAlgorithm(_encryptionAlgorithm)
                     .s3Request(request);
     
             EncryptionMaterials materials = _cryptoMaterialsManager.getEncryptionMaterials(requestBuilder.build());
    @@ -72,8 +77,8 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload
                         "This may be caused by a misconfigured custom CMM implementation, or " +
                         "a suppressed exception from CMM invocation due to a network failure.");
             }
    -
    -        CreateMultipartUploadRequest createMpuRequest = _contentMetadataEncodingStrategy.encodeMetadata(materials, encryptedContent.iv(), request);
    +        final byte[] contentIV = materials.algorithmSuite().isCommitting() ? materials.messageId() : materials.iv();
    +        CreateMultipartUploadRequest createMpuRequest = _contentMetadataEncodingStrategy.encodeMetadata(materials, contentIV, request);
             request = createMpuRequest.toBuilder()
                     .overrideConfiguration(API_NAME_INTERCEPTOR)
                     .build();
    @@ -86,6 +91,7 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload
                     .fromEncryptionMaterials(materials)
                     .cipher(encryptedContent.getCipher())
                     .build();
    +        mpuMaterials.setIvAndMessageId(encryptedContent.iv(), encryptedContent.messageId());
     
             _multipartUploadMaterials.put(response.uploadId(), mpuMaterials);
     
    @@ -144,7 +150,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ
             materials.beginPartUpload(actualRequest.partNumber(), partContentLength);
             //= specification/s3-encryption/client.md#optional-api-operations
             //# - Each part MUST be encrypted using the same cipher instance for each part.
    -        Cipher cipher = materials.getCipher(materials.getIv());
    +        Cipher cipher = materials.getCipher(materials.iv());
     
             ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
     
    @@ -157,7 +163,7 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ
                         partContentLength, // this MUST be the original contentLength; it refers to the plaintext stream
                         singleThreadExecutor
                     ),
    -                ciphertextLength, materials, cipher.getIV(), null, isLastPart
    +                ciphertextLength, materials, cipher.getIV(), materials.messageId(), isLastPart
                 );
     
                 // Ensure we haven't already seen the last part
    @@ -218,7 +224,7 @@ public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadReq
     
         public void putLocalObject(RequestBody requestBody, String uploadId, OutputStream os) throws IOException {
             final MultipartUploadMaterials materials = _multipartUploadMaterials.get(uploadId);
    -        Cipher cipher = materials.getCipher(materials.getIv());
    +        Cipher cipher = materials.getCipher(materials.iv());
             final InputStream cipherInputStream = new AuthenticatedCipherInputStream(requestBody.contentStreamProvider().newStream(), cipher);
     
             try {
    @@ -270,6 +276,11 @@ public Builder instructionFileConfig(InstructionFileConfig instructionFileConfig
                 return this;
             }
     
    +        public Builder encryptionAlgorithm(AlgorithmSuite encryptionAlgorithm) {
    +            this._encryptionAlgorithm = encryptionAlgorithm;
    +            return this;
    +        }
    +
             public MultipartUploadObjectPipeline build() {
                 // Default to AesGcm since it is the only active (non-legacy) content encryption strategy
                 if (_contentEncryptionStrategy == null) {
    
  • src/main/java/software/amazon/encryption/s3/internal/PutEncryptedObjectPipeline.java+13 2 modified
    @@ -24,6 +24,7 @@ public class PutEncryptedObjectPipeline {
         final private CryptographicMaterialsManager _cryptoMaterialsManager;
         final private AsyncContentEncryptionStrategy _asyncContentEncryptionStrategy;
         final private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy;
    +    final private AlgorithmSuite _encryptionAlgorithm;
     
         public static Builder builder() {
             return new Builder();
    @@ -34,6 +35,7 @@ private PutEncryptedObjectPipeline(Builder builder) {
             this._cryptoMaterialsManager = builder._cryptoMaterialsManager;
             this._asyncContentEncryptionStrategy = builder._asyncContentEncryptionStrategy;
             this._contentMetadataEncodingStrategy = builder._contentMetadataEncodingStrategy;
    +        this._encryptionAlgorithm = builder._encryptionAlgorithm;
         }
     
         public CompletableFuture<PutObjectResponse> putObject(PutObjectRequest request, AsyncRequestBody requestBody) {
    @@ -54,14 +56,17 @@ public CompletableFuture<PutObjectResponse> putObject(PutObjectRequest request,
                 contentLength = requestBody.contentLength().orElseThrow(() -> new S3EncryptionClientException("Unbounded streams are currently not supported."));
             }
     
    -        if (contentLength > AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherMaxContentLengthBytes()) {
    +        if (contentLength > _encryptionAlgorithm.cipherMaxContentLengthBytes()) {
                 throw new S3EncryptionClientException("The contentLength of the object you are attempting to encrypt exceeds" +
                         "the maximum length allowed for GCM encryption.");
             }
     
             EncryptionMaterialsRequest encryptionMaterialsRequest = EncryptionMaterialsRequest.builder()
                     .s3Request(request)
                     .plaintextLength(contentLength)
    +                //= specification/s3-encryption/encryption.md#content-encryption
    +                //# The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
    +                .encryptionAlgorithm(_encryptionAlgorithm)
                     .build();
     
             EncryptionMaterials materials = _cryptoMaterialsManager.getEncryptionMaterials(encryptionMaterialsRequest);
    @@ -73,7 +78,8 @@ public CompletableFuture<PutObjectResponse> putObject(PutObjectRequest request,
     
             EncryptedContent encryptedContent = _asyncContentEncryptionStrategy.encryptContent(materials, requestBody);
     
    -        PutObjectRequest modifiedRequest = _contentMetadataEncodingStrategy.encodeMetadata(materials, encryptedContent.iv(), request);
    +        final byte[] contentIV = materials.algorithmSuite().isCommitting() ? materials.messageId() : materials.iv();
    +        PutObjectRequest modifiedRequest = _contentMetadataEncodingStrategy.encodeMetadata(materials, contentIV, request);
             PutObjectRequest encryptedPutRequest = modifiedRequest.toBuilder()
                     .overrideConfiguration(API_NAME_INTERCEPTOR)
                     .contentLength(encryptedContent.getCiphertextLength())
    @@ -118,6 +124,11 @@ public Builder instructionFileConfig(InstructionFileConfig instructionFileConfig
                 return this;
             }
     
    +        public Builder encryptionAlgorithm(AlgorithmSuite encryptionAlgorithm) {
    +            this._encryptionAlgorithm = encryptionAlgorithm;
    +            return this;
    +        }
    +
             public PutEncryptedObjectPipeline build() {
                 // Default to AesGcm since it is the only active (non-legacy) content encryption strategy
                 if (_asyncContentEncryptionStrategy == null) {
    
  • src/main/java/software/amazon/encryption/s3/internal/StreamingAesGcmContentStrategy.java+33 7 modified
    @@ -3,6 +3,7 @@
     package software.amazon.encryption.s3.internal;
     
     import java.security.SecureRandom;
    +import java.util.Arrays;
     
     import javax.crypto.Cipher;
     
    @@ -32,14 +33,26 @@ public MultipartEncryptedContent initMultipartEncryption(EncryptionMaterials mat
                         "the maximum length allowed for GCM encryption.");
             }
     
    -
             //= specification/s3-encryption/encryption.md#content-encryption
             //# The client MUST generate an IV or Message ID using the length of the IV or Message ID defined in the algorithm suite.
             final byte[] iv = new byte[materials.algorithmSuite().iVLengthBytes()];
    -        _secureRandom.nextBytes(iv);
    +        final byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()];
    +        //= specification/s3-encryption/encryption.md#content-encryption
    +        //# The generated IV or Message ID MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        if (materials.algorithmSuite().isCommitting()) {
    +            // Set MessageId if the algorithm is commiting.
    +            //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +            //# When encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    +            //# the IV used in the AES-GCM content encryption/decryption MUST consist entirely of bytes with the value 0x01.
    +            Arrays.fill(iv, (byte) 0x01);
    +            _secureRandom.nextBytes(messageId);
    +        } else {
    +            _secureRandom.nextBytes(iv);
    +        }
    +        materials.setIvAndMessageId(iv, messageId);
     
    -        final Cipher cipher = CipherProvider.createAndInitCipher(materials, iv, null);
    -        return new MultipartEncryptedContent(iv, null, cipher, materials.getCiphertextLength());
    +        final Cipher cipher = CipherProvider.createAndInitCipher(materials, materials.iv(), materials.messageId());
    +        return new MultipartEncryptedContent(materials.iv(), materials.messageId(), cipher, materials.getCiphertextLength());
         }
     
         @Override
    @@ -54,10 +67,23 @@ public EncryptedContent encryptContent(EncryptionMaterials materials, AsyncReque
             //= specification/s3-encryption/encryption.md#content-encryption
             //# The client MUST generate an IV or Message ID using the length of the IV or Message ID defined in the algorithm suite.
             final byte[] iv = new byte[materials.algorithmSuite().iVLengthBytes()];
    -        _secureRandom.nextBytes(iv);
    +        final byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()];
    +        //= specification/s3-encryption/encryption.md#content-encryption
    +        //# The generated IV or Message ID MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        if (materials.algorithmSuite().isCommitting()) {
    +            // Set MessageId if the algorithm is commiting.
    +            //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +            //# When encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    +            //# the IV used in the AES-GCM content encryption/decryption MUST consist entirely of bytes with the value 0x01.
    +            Arrays.fill(iv, (byte) 0x01);
    +            _secureRandom.nextBytes(messageId);
    +        } else {
    +            _secureRandom.nextBytes(iv);
    +        }
    +        materials.setIvAndMessageId(iv, messageId);
     
    -        AsyncRequestBody encryptedAsyncRequestBody = new CipherAsyncRequestBody(content, materials.getCiphertextLength(), materials, iv, null);
    -        return new EncryptedContent(iv, null, encryptedAsyncRequestBody, materials.getCiphertextLength());
    +        AsyncRequestBody encryptedAsyncRequestBody = new CipherAsyncRequestBody(content, materials.getCiphertextLength(), materials, materials.iv(), materials.messageId());
    +        return new EncryptedContent(materials.iv(), materials.messageId(), encryptedAsyncRequestBody, materials.getCiphertextLength());
         }
     
         public static class Builder {
    
  • src/main/java/software/amazon/encryption/s3/materials/AesKeyring.java+6 1 modified
    @@ -120,7 +120,12 @@ public byte[] encryptDataKey(SecureRandom secureRandom,
                 final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
                 cipher.init(Cipher.ENCRYPT_MODE, _wrappingKey, gcmParameterSpec, secureRandom);
     
    -            final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
    +            byte[] aADBytes;
    +            if (materials.algorithmSuite().id() == AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.id()) {
    +                aADBytes = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.idAsString().getBytes(StandardCharsets.UTF_8);
    +            } else {
    +                aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
    +            }
                 cipher.updateAAD(aADBytes);
                 byte[] ciphertext = cipher.doFinal(materials.plaintextDataKey());
     
    
  • src/main/java/software/amazon/encryption/s3/materials/CryptographicMaterials.java+5 0 modified
    @@ -30,4 +30,9 @@ public interface CryptographicMaterials {
     
         Cipher getCipher(byte[] iv);
     
    +    byte[] getKeyCommitment();
    +
    +    byte[] messageId();
    +
    +    byte[] iv();
     }
    
  • src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java+3 5 modified
    @@ -40,7 +40,7 @@ final public class DecryptionMaterials implements CryptographicMaterials {
         final private long _ciphertextLength;
         final private Provider _cryptoProvider;
         final private String _contentRange;
    -    private byte[] _keyCommitment;
    +    final private byte[] _keyCommitment;
         private byte[] _messageId;
         private byte[] _iv;
     
    @@ -122,18 +122,16 @@ public String getContentRange() {
             return _contentRange;
         }
     
    -    public void setKeyCommitment(byte[] keyCommitment) {
    -        this._keyCommitment = keyCommitment;
    -    }
    -
         public byte[] getKeyCommitment() {
             return _keyCommitment != null ? _keyCommitment.clone() : null;
         }
     
    +    @Override
         public byte[] messageId() {
             return _messageId;
         }
     
    +    @Override
         public byte[] iv() {
             return _iv;
         }
    
  • src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java+12 0 modified
    @@ -19,6 +19,7 @@ public class DecryptMaterialsRequest {
         private final MaterialsDescription _materialsDescription;
         private final long _ciphertextLength;
         private final String _contentRange;
    +    private final byte[] _keyCommitment;
     
         private DecryptMaterialsRequest(Builder builder) {
             this._s3Request = builder._s3Request;
    @@ -28,6 +29,7 @@ private DecryptMaterialsRequest(Builder builder) {
             this._materialsDescription = builder._materialsDescription;
             this._ciphertextLength = builder._ciphertextLength;
             this._contentRange = builder._contentRange;
    +        this._keyCommitment = builder._keyCommitment;
         }
     
         static public Builder builder() {
    @@ -79,6 +81,10 @@ public String contentRange() {
             return _contentRange;
         }
     
    +    public byte[] keyCommitment() {
    +        return _keyCommitment;
    +    }
    +
         static public class Builder {
     
             public GetObjectRequest _s3Request = null;
    @@ -88,6 +94,7 @@ static public class Builder {
             private List<EncryptedDataKey> _encryptedDataKeys = Collections.emptyList();
             private long _ciphertextLength = -1;
             private String _contentRange = null;
    +        private byte[] _keyCommitment = null;
     
             private Builder() {
             }
    @@ -133,6 +140,11 @@ public Builder contentRange(String range) {
                 return this;
             }
     
    +        public Builder keyCommitment(byte[] keyCommitment) {
    +            _keyCommitment = keyCommitment;
    +            return this;
    +        }
    +
             public DecryptMaterialsRequest build() {
                 return new DecryptMaterialsRequest(this);
             }
    
  • src/main/java/software/amazon/encryption/s3/materials/DefaultCryptoMaterialsManager.java+2 3 modified
    @@ -2,8 +2,6 @@
     // SPDX-License-Identifier: Apache-2.0
     package software.amazon.encryption.s3.materials;
     
    -import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    -
     import java.security.Provider;
     
     /**
    @@ -26,7 +24,7 @@ public static Builder builder() {
         public EncryptionMaterials getEncryptionMaterials(EncryptionMaterialsRequest request) {
             EncryptionMaterials materials = EncryptionMaterials.builder()
                     .s3Request(request.s3Request())
    -                .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .algorithmSuite(request.encryptionAlgorithm())
                     .encryptionContext(request.encryptionContext())
                     .cryptoProvider(_cryptoProvider)
                     .plaintextLength(request.plaintextLength())
    @@ -44,6 +42,7 @@ public DecryptionMaterials decryptMaterials(DecryptMaterialsRequest request) {
                     .ciphertextLength(request.ciphertextLength())
                     .cryptoProvider(_cryptoProvider)
                     .contentRange(request.contentRange())
    +                .keyCommitment(request.keyCommitment())
                     .build();
     
             return _keyring.onDecrypt(materials, request.encryptedDataKeys());
    
  • src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java+55 6 modified
    @@ -12,6 +12,7 @@
     import javax.crypto.Cipher;
     import javax.crypto.SecretKey;
     import javax.crypto.spec.SecretKeySpec;
    +import java.security.MessageDigest;
     import java.security.Provider;
     import java.util.Collections;
     import java.util.List;
    @@ -40,6 +41,11 @@ final public class EncryptionMaterials implements CryptographicMaterials {
         private final Provider _cryptoProvider;
         private final long _plaintextLength;
         private final long _ciphertextLength;
    +    // Key Commitment is set during the encryption process
    +    private byte[] _keyCommitment;
    +    private byte[] _iv;
    +    private byte[] _messageId;
    +    private Cipher _cipher;
     
         private EncryptionMaterials(Builder builder) {
             this._s3Request = builder._s3Request;
    @@ -71,7 +77,7 @@ public AlgorithmSuite algorithmSuite() {
          */
         @Override
         @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
    -        + " implementation is immutable")
    +            + " implementation is immutable")
         public Map<String, String> encryptionContext() {
             return _encryptionContext;
         }
    @@ -81,7 +87,7 @@ public Map<String, String> encryptionContext() {
          * immutable.
          */
         @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
    -        + " implementation is immutable")
    +            + " implementation is immutable")
         public List<EncryptedDataKey> encryptedDataKeys() {
             return _encryptedDataKeys;
         }
    @@ -120,7 +126,35 @@ public CipherMode cipherMode() {
     
         @Override
         public Cipher getCipher(byte[] iv) {
    -        return CipherProvider.createAndInitCipher(this, iv, null);
    +        if (!MessageDigest.isEqual(iv, _iv)) {
    +            throw new S3EncryptionClientException("IV does not match!");
    +        }
    +        return _cipher;
    +    }
    +
    +    public byte[] getKeyCommitment() {
    +        return _keyCommitment != null ? _keyCommitment.clone() : null;
    +    }
    +
    +    public void setKeyCommitment(byte[] keyCommitment) {
    +        _keyCommitment = keyCommitment;
    +    }
    +
    +    @Override
    +    public byte[] messageId() {
    +        return _messageId != null ? _messageId.clone() : null;
    +    }
    +
    +    @Override
    +    public byte[] iv() {
    +        return _iv != null ? _iv.clone() : null;
    +    }
    +
    +    public void setIvAndMessageId(byte[] iv, byte[] messageId) {
    +        this._iv = iv;
    +        this._messageId = messageId;
    +        // Once we have an IV, we can create a cipher
    +        this._cipher = CipherProvider.createAndInitCipher(this, iv, messageId);
         }
     
         public Builder toBuilder() {
    @@ -132,20 +166,22 @@ public Builder toBuilder() {
                     .plaintextDataKey(_plaintextDataKey)
                     .cryptoProvider(_cryptoProvider)
                     .materialsDescription(_materialsDescription)
    -                .plaintextLength(_plaintextLength);
    +                .plaintextLength(_plaintextLength)
    +                .keyCommitment(_keyCommitment);
         }
     
         static public class Builder {
     
             private S3Request _s3Request = null;
    -
    -        private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
    +        private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
             private Map<String, String> _encryptionContext = Collections.emptyMap();
             private List<EncryptedDataKey> _encryptedDataKeys = Collections.emptyList();
             private byte[] _plaintextDataKey = null;
             private long _plaintextLength = -1;
             private Provider _cryptoProvider = null;
             private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build();
    +        private byte[] _keyCommitment = null;
    +        private byte[] _iv = null;
     
             private Builder() {
             }
    @@ -159,12 +195,14 @@ public Builder algorithmSuite(AlgorithmSuite algorithmSuite) {
                 _algorithmSuite = algorithmSuite;
                 return this;
             }
    +
             public Builder materialsDescription(MaterialsDescription materialsDescription) {
                 _materialsDescription = materialsDescription == null
                         ? MaterialsDescription.builder().build()
                         : materialsDescription;
                 return this;
             }
    +
             public Builder encryptionContext(Map<String, String> encryptionContext) {
                 _encryptionContext = encryptionContext == null
                         ? Collections.emptyMap()
    @@ -183,6 +221,7 @@ public Builder plaintextDataKey(byte[] plaintextDataKey) {
                 _plaintextDataKey = plaintextDataKey == null ? null : plaintextDataKey.clone();
                 return this;
             }
    +
             public Builder cryptoProvider(Provider cryptoProvider) {
                 _cryptoProvider = cryptoProvider;
                 return this;
    @@ -193,6 +232,16 @@ public Builder plaintextLength(long plaintextLength) {
                 return this;
             }
     
    +        public Builder keyCommitment(byte[] keyCommitment) {
    +            _keyCommitment = keyCommitment;
    +            return this;
    +        }
    +
    +        public Builder iV(byte[] iv) {
    +            _iv = iv;
    +            return this;
    +        }
    +
             public EncryptionMaterials build() {
                 if (!_materialsDescription.isEmpty() && !_encryptionContext.isEmpty()) {
                     throw new S3EncryptionClientException("MaterialsDescription and EncryptionContext cannot both be set!");
    
  • src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterialsRequest.java+14 2 modified
    @@ -3,22 +3,24 @@
     package software.amazon.encryption.s3.materials;
     
     import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    +import software.amazon.awssdk.services.s3.model.S3Request;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     
     import java.util.Collections;
     import java.util.Map;
     
    -import software.amazon.awssdk.services.s3.model.S3Request;
    -
     final public class EncryptionMaterialsRequest {
     
         private final S3Request _s3Request;
         private final Map<String, String> _encryptionContext;
         private final long _plaintextLength;
    +    private final AlgorithmSuite _encryptionAlgorithm;
     
         private EncryptionMaterialsRequest(Builder builder) {
             this._s3Request = builder._s3Request;
             this._encryptionContext = builder._encryptionContext;
             this._plaintextLength = builder._plaintextLength;
    +        this._encryptionAlgorithm = builder._encryptionAlgorithm;
         }
     
         static public Builder builder() {
    @@ -33,6 +35,10 @@ public long plaintextLength() {
             return _plaintextLength;
         }
     
    +    public AlgorithmSuite encryptionAlgorithm() {
    +        return _encryptionAlgorithm;
    +    }
    +
         /**
          * Note that the underlying implementation uses a Collections.unmodifiableMap which is
          * immutable.
    @@ -48,6 +54,7 @@ static public class Builder {
             public S3Request _s3Request = null;
             private Map<String, String> _encryptionContext = Collections.emptyMap();
             private long _plaintextLength = -1;
    +        private AlgorithmSuite _encryptionAlgorithm = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
     
             private Builder() {
             }
    @@ -69,6 +76,11 @@ public Builder plaintextLength(final long plaintextLength) {
                 return this;
             }
     
    +        public Builder encryptionAlgorithm(AlgorithmSuite encryptionAlgorithm) {
    +            _encryptionAlgorithm = encryptionAlgorithm;
    +            return this;
    +        }
    +
             public EncryptionMaterialsRequest build() {
                 return new EncryptionMaterialsRequest(this);
             }
    
  • src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java+1 1 modified
    @@ -112,6 +112,7 @@ public boolean isLegacy() {
     
             @Override
             public String keyProviderInfo() {
    +            // Default to v3 format for new encryptions
                 return KEY_PROVIDER_INFO;
             }
     
    @@ -138,7 +139,6 @@ public EncryptionMaterials modifyMaterials(EncryptionMaterials materials) {
                 } else {
                     encryptionContext.put(ENCRYPTION_CONTEXT_ALGORITHM_KEY, materials.algorithmSuite().cipherName());
                 }
    -
                 return materials.toBuilder()
                         .encryptionContext(encryptionContext)
                         .build();
    
  • src/main/java/software/amazon/encryption/s3/materials/RsaKeyring.java+8 3 modified
    @@ -122,12 +122,17 @@ public byte[] encryptDataKey(SecureRandom secureRandom,
     
                 // Create a pseudo-data key with the content encryption appended to the data key
                 byte[] dataKey = materials.plaintextDataKey();
    -            byte[] dataCipherName = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
    -            byte[] pseudoDataKey = new byte[1 + dataKey.length + dataCipherName.length];
    +            byte[] dataCipherAlg;
    +            if (materials.algorithmSuite().id() == AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.id()) {
    +                dataCipherAlg = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.idAsString().getBytes(StandardCharsets.UTF_8);
    +            } else {
    +                dataCipherAlg = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
    +            }
    +            byte[] pseudoDataKey = new byte[1 + dataKey.length + dataCipherAlg.length];
     
                 pseudoDataKey[0] = (byte) dataKey.length;
                 System.arraycopy(dataKey, 0, pseudoDataKey, 1, dataKey.length);
    -            System.arraycopy(dataCipherName, 0, pseudoDataKey, 1 + dataKey.length, dataCipherName.length);
    +            System.arraycopy(dataCipherAlg, 0, pseudoDataKey, 1 + dataKey.length, dataCipherAlg.length);
     
                 byte[] ciphertext = cipher.wrap(new SecretKeySpec(pseudoDataKey, materials.algorithmSuite().dataKeyAlgorithm()));
                 return ciphertext;
    
  • src/main/java/software/amazon/encryption/s3/S3AsyncEncryptionClient.java+24 34 modified
    @@ -85,6 +85,7 @@ public class S3AsyncEncryptionClient extends DelegatingS3AsyncClient {
         private final boolean _enableMultipartPutObject;
         private final long _bufferSize;
         private final InstructionFileConfig _instructionFileConfig;
    +    private final AlgorithmSuite _encryptionAlgorithm;
         private final CommitmentPolicy _commitmentPolicy;
     
         private S3AsyncEncryptionClient(Builder builder) {
    @@ -97,29 +98,18 @@ private S3AsyncEncryptionClient(Builder builder) {
             _enableMultipartPutObject = builder._enableMultipartPutObject;
             _bufferSize = builder._bufferSize;
             _instructionFileConfig = builder._instructionFileConfig;
    +        _encryptionAlgorithm = builder._encryptionAlgorithm;
             _commitmentPolicy = builder._commitmentPolicy;
         }
     
         /**
          * Creates a builder that can be used to configure and create a {@link S3AsyncEncryptionClient}.
          */
    -    @Deprecated
    -    public static Builder builder() {
    -        // Defaults to ForbidEncryptAllowDecrypt and ALG_AES_256_GCM_IV12_TAG16_NO_KDF
    -        return new Builder(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF);
    -    }
    -
    -
    -
    -    /**
    -     * Creates a V4 Transition builder that can be used to configure and create a {@link S3AsyncEncryptionClient}.
    -     */
         public static Builder builderV4() {
    -        // NewBuilder introduced which REQUIRES commitmentPolicy and encryptionAlgorithm
    -        // to be set and is V4 compatible
             return new Builder();
         }
     
    +
         /**
          * Attaches encryption context to a request. Must be used as a parameter to
          * {@link S3Request#overrideConfiguration()} in the request.
    @@ -171,6 +161,7 @@ public CompletableFuture<PutObjectResponse> putObject(PutObjectRequest putObject
                     .cryptoMaterialsManager(_cryptoMaterialsManager)
                     .secureRandom(_secureRandom)
                     .instructionFileConfig(_instructionFileConfig)
    +                .encryptionAlgorithm(_encryptionAlgorithm)
                     .build();
     
             return pipeline.putObject(putObjectRequest, requestBody);
    @@ -190,6 +181,7 @@ private CompletableFuture<PutObjectResponse> multipartPutObject(PutObjectRequest
                     .cryptoMaterialsManager(_cryptoMaterialsManager)
                     .secureRandom(_secureRandom)
                     .instructionFileConfig(_instructionFileConfig)
    +                .encryptionAlgorithm(_encryptionAlgorithm)
                     .build();
             // Ensures parts are not retried to avoid corrupting ciphertext
             AsyncRequestBody noRetryBody = new NoRetriesAsyncRequestBody(requestBody);
    @@ -329,8 +321,8 @@ public static class Builder implements S3AsyncClientBuilder {
             private SecureRandom _secureRandom = new SecureRandom();
             private long _bufferSize = -1L;
             private InstructionFileConfig _instructionFileConfig = null;
    -        private AlgorithmSuite _encryptionAlgorithm = null;
    -        private CommitmentPolicy _commitmentPolicy = null;
    +        private AlgorithmSuite _encryptionAlgorithm = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
    +        private CommitmentPolicy _commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT;
     
             // generic AwsClient configuration to be shared by default clients
             private AwsCredentialsProvider _awsCredentialsProvider = null;
    @@ -357,11 +349,6 @@ public static class Builder implements S3AsyncClientBuilder {
             private Builder() {
             }
     
    -        private Builder(CommitmentPolicy commitmentPolicy, AlgorithmSuite algorithmSuite) {
    -            _commitmentPolicy = commitmentPolicy;
    -            _encryptionAlgorithm = algorithmSuite;
    -        }
    -
             /**
              * Specifies the wrapped client to use for the actual S3 request.
              * This client will be used for all async operations.
    @@ -961,26 +948,29 @@ public S3AsyncEncryptionClient build() {
     
                 //= specification/s3-encryption/client.md#encryption-algorithm
                 //# The S3EC MUST validate that the configured encryption algorithm is not legacy.
    +            if (_encryptionAlgorithm.isLegacy()) {
    +                //= specification/s3-encryption/client.md#encryption-algorithm
    +                //# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception.
    +                throw new S3EncryptionClientException("Encryption algorithm provided is LEGACY! Please specify a fully-supported encryption algorithm.");
    +            }
    +
                 //= specification/s3-encryption/client.md#key-commitment
                 //# The S3EC MUST validate the configured Encryption Algorithm against the provided key commitment policy.
    +            if (_commitmentPolicy.requiresEncrypt() && !_encryptionAlgorithm.isCommitting()) {
    +                //= specification/s3-encryption/client.md#key-commitment
    +                //# If the configured Encryption Algorithm is incompatible with the key commitment policy, then it MUST throw an exception.
    +                throw new S3EncryptionClientException("The commitment policy requires encryption with a committing algorithm suite, " +
    +                        "but the specified encryption algorithm does not support key commitment. " +
    +                        "Please configure a committing algorithm suite such as ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.");
    +            }
                 //= specification/s3-encryption/key-commitment.md#commitment-policy
                 //# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST NOT encrypt using an algorithm suite which supports key commitment.
    -            if (_encryptionAlgorithm == null || _commitmentPolicy == null) {
    -                throw new S3EncryptionClientException(
    -                        "Both encryption algorithm and commitment policy must be configured."
    -                );
    -            }
    -            if (!(_encryptionAlgorithm.id() == AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.id()
    -                    && _commitmentPolicy.equals(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT))) {
    -                //= specification/s3-encryption/client.md#encryption-algorithm
    -                //# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception.
    +            if (_commitmentPolicy.equals(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) && _encryptionAlgorithm.isCommitting()) {
                     //= specification/s3-encryption/client.md#key-commitment
                     //# If the configured Encryption Algorithm is incompatible with the key commitment policy, then it MUST throw an exception.
    -                throw new S3EncryptionClientException(
    -                        "This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF. "
    -                                + "Note that the Encryption Algorithm does NOT effect Decryption; See: https://docs.aws.amazon.com/amazon-s3-encryption-client/latest/developerguide/encryption-algorithms.html#decryption-modes. "
    -                                +  "Please update your configuration. Provided algorithm: " + _encryptionAlgorithm.name() + " and commitment policy: " + _commitmentPolicy.name()
    -                );
    +                throw new S3EncryptionClientException("The commitment policy forbids encryption with committing algorithm suites, " +
    +                        "but the specified encryption algorithm supports key commitment. " +
    +                        "Please configure a non-committing algorithm suite such as ALG_AES_256_GCM_IV12_TAG16_NO_KDF or change the commitment policy.");
                 }
     
                 return new S3AsyncEncryptionClient(this);
    
  • src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java+100 130 modified
    @@ -2,8 +2,36 @@
     // SPDX-License-Identifier: Apache-2.0
     package software.amazon.encryption.s3;
     
    -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_BUFFER_SIZE_BYTES;
    +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
    +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MAX_ALLOWED_BUFFER_SIZE_BYTES;
    +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MIN_ALLOWED_BUFFER_SIZE_BYTES;
    +import static software.amazon.encryption.s3.S3EncryptionClientUtilities.instructionFileKeysToDelete;
    +import static software.amazon.encryption.s3.internal.ApiNameVersion.API_NAME_INTERCEPTOR;
    +
    +import java.io.IOException;
    +import java.net.URI;
    +import java.security.KeyPair;
    +import java.security.Provider;
    +import java.security.SecureRandom;
    +import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Optional;
    +import java.util.concurrent.CompletableFuture;
    +import java.util.concurrent.CompletionException;
    +import java.util.concurrent.ExecutionException;
    +import java.util.concurrent.ExecutorService;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.Future;
    +import java.util.function.Consumer;
    +
    +import javax.crypto.SecretKey;
    +
     import org.apache.commons.logging.LogFactory;
    +
    +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
     import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
     import software.amazon.awssdk.awscore.exception.AwsServiceException;
    @@ -68,45 +96,18 @@
     import software.amazon.encryption.s3.materials.EncryptionMaterials;
     import software.amazon.encryption.s3.materials.Keyring;
     import software.amazon.encryption.s3.materials.KmsKeyring;
    -import software.amazon.encryption.s3.materials.MaterialsDescription;
     import software.amazon.encryption.s3.materials.MultipartConfiguration;
     import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
     import software.amazon.encryption.s3.materials.RawKeyring;
     import software.amazon.encryption.s3.materials.RsaKeyring;
     import software.amazon.encryption.s3.materials.S3Keyring;
    -
    -import javax.crypto.SecretKey;
    -import java.io.IOException;
    -import java.net.URI;
    -import java.security.KeyPair;
    -import java.security.Provider;
    -import java.security.SecureRandom;
    -import java.util.ArrayList;
    -import java.util.Collections;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.Optional;
    -import java.util.concurrent.CompletableFuture;
    -import java.util.concurrent.CompletionException;
    -import java.util.concurrent.ExecutionException;
    -import java.util.concurrent.ExecutorService;
    -import java.util.concurrent.Executors;
    -import java.util.concurrent.Future;
    -import java.util.function.Consumer;
    -
    -import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_BUFFER_SIZE_BYTES;
    -import static software.amazon.encryption.s3.S3EncryptionClientUtilities.DEFAULT_INSTRUCTION_FILE_SUFFIX;
    -import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MAX_ALLOWED_BUFFER_SIZE_BYTES;
    -import static software.amazon.encryption.s3.S3EncryptionClientUtilities.MIN_ALLOWED_BUFFER_SIZE_BYTES;
    -import static software.amazon.encryption.s3.S3EncryptionClientUtilities.instructionFileKeysToDelete;
    -import static software.amazon.encryption.s3.internal.ApiNameVersion.API_NAME_INTERCEPTOR;
    +import software.amazon.encryption.s3.materials.MaterialsDescription;
     
     
     //= specification/s3-encryption/client.md#aws-sdk-compatibility
     //# The S3EC MUST adhere to the same interface for API operations as the conventional AWS SDK S3 client.
     //= specification/s3-encryption/client.md#aws-sdk-compatibility
     //# The S3EC SHOULD support invoking operations unrelated to client-side encryption e.g. CopyObject as the conventional AWS SDK S3 client would.
    -
     /**
      * This client is a drop-in replacement for the S3 client. It will automatically encrypt objects
      * on putObject and decrypt objects on getObject using the provided encryption key(s).
    @@ -135,6 +136,7 @@ public class S3EncryptionClient extends DelegatingS3Client {
         private final MultipartUploadObjectPipeline _multipartPipeline;
         private final long _bufferSize;
         private final InstructionFileConfig _instructionFileConfig;
    +    private final AlgorithmSuite _encryptionAlgorithm;
         private final CommitmentPolicy _commitmentPolicy;
     
         //= specification/s3-encryption/client.md#aws-sdk-compatibility
    @@ -151,25 +153,14 @@ private S3EncryptionClient(Builder builder) {
             _multipartPipeline = builder._multipartPipeline;
             _bufferSize = builder._bufferSize;
             _instructionFileConfig = builder._instructionFileConfig;
    +        _encryptionAlgorithm = builder._encryptionAlgorithm;
             _commitmentPolicy = builder._commitmentPolicy;
         }
     
         /**
          * Creates a builder that can be used to configure and create a {@link S3EncryptionClient}.
          */
    -    @Deprecated
    -    public static Builder builder() {
    -        // Defaults to ForbidEncryptAllowDecrypt and ALG_AES_256_GCM_IV12_TAG16_NO_KDF
    -        return new Builder(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF);
    -    }
    -
    -
    -    /**
    -     * Creates a V4 Transition builder that can be used to configure and create a {@link S3EncryptionClient}.
    -     */
         public static Builder builderV4() {
    -        // NewBuilder introduced which REQUIRES commitmentPolicy and encryptionAlgorithm
    -        // to be set and is V4 compatible
             return new Builder();
         }
     
    @@ -179,7 +170,6 @@ public static Builder builderV4() {
          * Encryption context can be used to enforce authentication of ciphertext.
          * The same encryption context used to encrypt MUST be provided on decrypt.
          * Encryption context is only supported with KMS keys.
    -     *
          * @param encryptionContext the encryption context to use for the request.
          * @return Consumer for use in overrideConfiguration()
          */
    @@ -192,7 +182,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
          * Attaches a custom instruction file suffix to a request. Must be used as a parameter to
          * {@link S3Request#overrideConfiguration()} in the request.
          * This allows specifying a custom suffix for the instruction file on a per-request basis.
    -     *
          * @param customInstructionFileSuffix the custom suffix to use for the instruction file.
          * @return Consumer for use in overrideConfiguration()
          */
    @@ -204,7 +193,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withCustomInstru
         /**
          * Attaches multipart configuration to a request. Must be used as a parameter to
          * {@link S3Request#overrideConfiguration()} in the request.
    -     *
          * @param multipartConfiguration the {@link MultipartConfiguration} instance to use
          * @return Consumer for use in overrideConfiguration()
          */
    @@ -220,8 +208,7 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
          * Encryption context can be used to enforce authentication of ciphertext.
          * The same encryption context used to encrypt MUST be provided on decrypt.
          * Encryption context is only supported with KMS keys.
    -     *
    -     * @param encryptionContext      the encryption context to use for the request.
    +     * @param encryptionContext the encryption context to use for the request.
          * @param multipartConfiguration the {@link MultipartConfiguration} instance to use
          * @return Consumer for use in overrideConfiguration()
          */
    @@ -235,7 +222,6 @@ public static Consumer<AwsRequestOverrideConfiguration.Builder> withAdditionalCo
         //# - ReEncryptInstructionFile MAY be implemented by the S3EC.
         //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
         //# The S3EC MAY support re-encryption/key rotation via Instruction Files.
    -
         /**
          * Re-encrypts an instruction file with a new keyring while preserving the original encrypted object in S3.
          * This enables:
    @@ -268,16 +254,38 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru
     
             // Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption
             final AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
    +
    +        if (algorithmSuite.id() == AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.id() && _commitmentPolicy.requiresEncrypt()) {
    +            // TODO: Better Error Messages
    +            throw new S3EncryptionClientException("Given object is encrypted with legacy content encryption algorithm: " + algorithmSuite.cipherName() + ". " +
    +                    "To re-encrypt the data key, configure the client with encryptionAlgorithm ALG_AES_256_GCM_IV12_TAG16_NO_KDF and " +
    +                    "commitmentPolicy FORBID_ENCRYPT_ALLOW_DECRYPT and enable legacy unauthenticated modes to use legacy content decryption.");
    +        }
    +
    +        // Validate that object's algorithm matches the client's configured encryption algorithm
    +        if (algorithmSuite.id() != _encryptionAlgorithm.id()
    +                && algorithmSuite.id() != AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.id()) {
    +            if (algorithmSuite.id() == AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.id()) {
    +                // V2 format object - non-committing
    +                // TODO: Better Error Messages
    +                throw new S3EncryptionClientException("Given object is encrypted with non-committing encryption algorithm: " + algorithmSuite.cipherName() + ". " +
    +                        "To re-encrypt the data key, configure the client with encryptionAlgorithm ALG_AES_256_GCM_IV12_TAG16_NO_KDF and " +
    +                        "commitmentPolicy FORBID_ENCRYPT_ALLOW_DECRYPT.");
    +            } if (algorithmSuite.id() == AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.id()) {
    +                // V3 format object - committing
    +                // TODO: Better Error Messages
    +                throw new S3EncryptionClientException("Given object is encrypted with a committing encryption algorithm: " + algorithmSuite.cipherName() + ". " +
    +                        "To re-encrypt the data key, configure the client with encryptionAlgorithm ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY and " +
    +                        "commitmentPolicy REQUIRE_ENCRYPT_ALLOW_DECRYPT or REQUIRE_ENCRYPT_REQUIRE_DECRYPT.");
    +            } else {
    +                throw new S3EncryptionClientException("Unsupported algorithm: " + algorithmSuite.name());
    +            }
    +        }
    +
             final EncryptedDataKey originalEncryptedDataKey = contentMetadata.encryptedDataKey();
             final MaterialsDescription currentKeyringMaterialsDescription = contentMetadata.materialsDescription();
             final byte[] contentIv = contentMetadata.contentIv();
     
    -        if (algorithmSuite.isCommitting()) {
    -            // TODO: Proper Error Message
    -            throw new S3EncryptionClientException("Object encrypted with Algorithm Suite which support key commitment. " +
    -                    "This version of S3EC does not support committing");
    -        }
    -
             //Decrypt the data key using the current keyring
             //= specification/s3-encryption/client.md#optional-api-operations
             //# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM.
    @@ -316,8 +324,12 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru
                 enforceRotation(encryptedMaterials, request);
             }
     
    +        //We will keep the original key Commitment
    +        encryptedMaterials.setKeyCommitment(contentMetadata.keyCommitment());
    +
             //Create or update instruction file with the re-encrypted metadata while preserving IV
             ContentMetadataEncodingStrategy encodeStrategy = new ContentMetadataEncodingStrategy(_instructionFileConfig);
    +
             final byte[] contentIV = algorithmSuite.isCommitting() ? contentMetadata.contentMessageId() : contentMetadata.contentIv();
             encodeStrategy.encodeMetadata(encryptedMaterials, contentIV, PutObjectRequest.builder()
                     .bucket(reEncryptInstructionFileRequest.bucket())
    @@ -346,20 +358,19 @@ private void enforceRotation(EncryptionMaterials newEncryptionMaterials, GetObje
     
         //= specification/s3-encryption/client.md#required-api-operations
         //# - PutObject MUST be implemented by the S3EC.
    -
         /**
          * See {@link S3EncryptionClient#putObject(PutObjectRequest, RequestBody)}.
          * <p>
          * In the S3EncryptionClient, putObject encrypts the data in the requestBody as it is
          * written to S3.
          * </p>
    -     *
          * @param putObjectRequest the request instance
    -     * @param requestBody      The content to send to the service. A {@link RequestBody} can be created using one of several factory
    -     *                         methods for various sources of data. For example, to create a request body from a file you can do the
    -     *                         following.
    +     * @param requestBody
    +     *        The content to send to the service. A {@link RequestBody} can be created using one of several factory
    +     *        methods for various sources of data. For example, to create a request body from a file you can do the
    +     *        following.
          * @return Result of the PutObject operation returned by the service.
    -     * @throws SdkClientException          If any client side error occurs such as an IO related failure, failure to get credentials, etc.
    +     * @throws SdkClientException If any client side error occurs such as an IO related failure, failure to get credentials, etc.
          * @throws S3EncryptionClientException Base class for all encryption client exceptions.
          */
         @Override
    @@ -379,6 +390,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod
                     .cryptoMaterialsManager(_cryptoMaterialsManager)
                     .secureRandom(_secureRandom)
                     .instructionFileConfig(_instructionFileConfig)
    +                .encryptionAlgorithm(_encryptionAlgorithm)
                     .build();
     
             ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    @@ -412,21 +424,20 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod
     
         //= specification/s3-encryption/client.md#required-api-operations
         //# - GetObject MUST be implemented by the S3EC.
    -
         /**
          * See {@link S3EncryptionClient#getObject(GetObjectRequest, ResponseTransformer)}
          * <p>
          * In the S3EncryptionClient, getObject decrypts the data as it is read from S3.
          * </p>
    -     *
    -     * @param getObjectRequest    the request instance
    -     * @param responseTransformer Functional interface for processing the streamed response content. The unmarshalled GetObjectResponse and
    -     *                            an InputStream to the response content are provided as parameters to the callback. The callback may return
    -     *                            a transformed type which will be the return value of this method. See
    -     *                            {@link software.amazon.awssdk.core.sync.ResponseTransformer} for details on implementing this interface
    -     *                            and for links to pre-canned implementations for common scenarios like downloading to a file.
    +     * @param getObjectRequest the request instance
    +     * @param responseTransformer
    +     *        Functional interface for processing the streamed response content. The unmarshalled GetObjectResponse and
    +     *        an InputStream to the response content are provided as parameters to the callback. The callback may return
    +     *        a transformed type which will be the return value of this method. See
    +     *        {@link software.amazon.awssdk.core.sync.ResponseTransformer} for details on implementing this interface
    +     *        and for links to pre-canned implementations for common scenarios like downloading to a file.
          * @return The transformed result of the ResponseTransformer.
    -     * @throws SdkClientException          If any client side error occurs such as an IO related failure, failure to get credentials, etc.
    +     * @throws SdkClientException If any client side error occurs such as an IO related failure, failure to get credentials, etc.
          * @throws S3EncryptionClientException Base class for all encryption client exceptions.
          */
         @Override
    @@ -546,14 +557,12 @@ private <T extends Throwable> T onAbort(UploadObjectObserver observer, T t) {
     
         //= specification/s3-encryption/client.md#required-api-operations
         //# - DeleteObject MUST be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#deleteObject(DeleteObjectRequest)}.
          * <p>
          * In the S3 Encryption Client, deleteObject also deletes the instruction file,
          * if present.
          * </p>
    -     *
          * @param deleteObjectRequest the request instance
          * @return Result of the DeleteObject operation returned by the service.
          */
    @@ -588,14 +597,12 @@ public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest
     
         //= specification/s3-encryption/client.md#required-api-operations
         //# - DeleteObjects MUST be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#deleteObjects(DeleteObjectsRequest)}.
          * <p>
          * In the S3 Encryption Client, deleteObjects also deletes the instruction file(s),
          * if present.
          * </p>
    -     *
          * @param deleteObjectsRequest the request instance
          * @return Result of the DeleteObjects operation returned by the service.
          */
    @@ -629,15 +636,13 @@ public DeleteObjectsResponse deleteObjects(DeleteObjectsRequest deleteObjectsReq
     
         //= specification/s3-encryption/client.md#optional-api-operations
         //# - CreateMultipartUpload MAY be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#createMultipartUpload(CreateMultipartUploadRequest)}
          * <p>
          * In the S3EncryptionClient, createMultipartUpload creates an encrypted
          * multipart upload. Parts MUST be uploaded sequentially.
          * See {@link S3EncryptionClient#uploadPart(UploadPartRequest, RequestBody)} for details.
          * </p>
    -     *
          * @param request the request instance
          * @return Result of the CreateMultipartUpload operation returned by the service.
          */
    @@ -654,7 +659,6 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload
     
         //= specification/s3-encryption/client.md#optional-api-operations
         //# - UploadPart MAY be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#uploadPart(UploadPartRequest, RequestBody)}
          *
    @@ -663,7 +667,6 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload
          * S3EncryptionClient (as opposed to the normal S3Client) must
          * be uploaded serially, and in order. Otherwise, the previous encryption
          * context isn't available to use when encrypting the current part.
    -     *
          * @param request the request instance
          * @return Result of the UploadPart operation returned by the service.
          */
    @@ -681,10 +684,8 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ
     
         //= specification/s3-encryption/client.md#optional-api-operations
         //# - CompleteMultipartUpload MAY be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#completeMultipartUpload(CompleteMultipartUploadRequest)}
    -     *
          * @param request the request instance
          * @return Result of the CompleteMultipartUpload operation returned by the service.
          */
    @@ -702,10 +703,8 @@ public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipart
     
         //= specification/s3-encryption/client.md#optional-api-operations
         //# - AbortMultipartUpload MAY be implemented by the S3EC.
    -
         /**
          * See {@link S3Client#abortMultipartUpload(AbortMultipartUploadRequest)}
    -     *
          * @param request the request instance
          * @return Result of the AbortMultipartUpload operation returned by the service.
          */
    @@ -758,8 +757,8 @@ public static class Builder implements S3BaseClientBuilder<Builder, S3Encryption
             private boolean _enableLegacyUnauthenticatedModes = false;
             private long _bufferSize = -1L;
             private InstructionFileConfig _instructionFileConfig = null;
    -        private AlgorithmSuite _encryptionAlgorithm = null;
    -        private CommitmentPolicy _commitmentPolicy = null;
    +        private AlgorithmSuite _encryptionAlgorithm = AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
    +        private CommitmentPolicy _commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT;
             // generic AwsClient configuration to be shared by default clients
             private AwsCredentialsProvider _awsCredentialsProvider = null;
             private Region _region = null;
    @@ -783,15 +782,9 @@ public static class Builder implements S3BaseClientBuilder<Builder, S3Encryption
             private Builder() {
             }
     
    -        private Builder(CommitmentPolicy commitmentPolicy, AlgorithmSuite algorithmSuite) {
    -            _commitmentPolicy = commitmentPolicy;
    -            _encryptionAlgorithm = algorithmSuite;
    -        }
    -
             //= specification/s3-encryption/client.md#wrapped-s3-client-s
             //= type=implementation
             //# The S3EC MUST support the option to provide an SDK S3 client instance during its initialization.
    -
             /**
              * Sets the wrappedClient to be used for non-cryptographic operations.
              */
    @@ -814,7 +807,6 @@ public Builder wrappedClient(S3Client _wrappedClient) {
             //= specification/s3-encryption/client.md#wrapped-s3-client-s
             //= type=implementation
             //# The S3EC MUST support the option to provide an SDK S3 client instance during its initialization.
    -
             /**
              * Sets the wrappedAsyncClient to be used for cryptographic operations.
              */
    @@ -837,7 +829,6 @@ public Builder wrappedAsyncClient(S3AsyncClient _wrappedAsyncClient) {
     
             /**
              * Specifies the {@link CryptographicMaterialsManager} to use for managing key wrapping keys.
    -         *
              * @param cryptoMaterialsManager the CMM to use
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -850,7 +841,6 @@ public Builder cryptoMaterialsManager(CryptographicMaterialsManager cryptoMateri
     
             /**
              * Specifies the {@link Keyring} to use for key wrapping and unwrapping.
    -         *
              * @param keyring the Keyring instance to use
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -863,7 +853,6 @@ public Builder keyring(Keyring keyring) {
     
             /**
              * Specifies a "raw" AES key to use for key wrapping/unwrapping.
    -         *
              * @param aesKey the AES key as a {@link SecretKey} instance
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -876,7 +865,6 @@ public Builder aesKey(SecretKey aesKey) {
     
             /**
              * Specifies a "raw" RSA key pair to use for key wrapping/unwrapping.
    -         *
              * @param rsaKeyPair the RSA key pair as a {@link KeyPair} instance
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -892,7 +880,6 @@ public Builder rsaKeyPair(KeyPair rsaKeyPair) {
              * This option takes a {@link PartialRsaKeyPair} instance, which allows
              * either a public key (decryption only) or private key (encryption only)
              * rather than requiring both parts.
    -         *
              * @param partialRsaKeyPair the RSA key pair as a {@link PartialRsaKeyPair} instance
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -908,7 +895,6 @@ public Builder rsaKeyPair(PartialRsaKeyPair partialRsaKeyPair) {
              * identifier (including the full ARN or an alias ARN) is permitted. When
              * decrypting objects, the key referred to by this KMS key identifier is
              * always used.
    -         *
              * @param kmsKeyId the KMS key identifier as a {@link String} instance
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -952,11 +938,9 @@ private boolean onlyOneNonNull(Object... values) {
     
             //= specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
             //# The S3EC MUST support the option to enable or disable legacy wrapping algorithms.
    -
             /**
              * When set to true, decryption of objects using legacy key wrapping
              * modes is enabled.
    -         *
              * @param shouldEnableLegacyWrappingAlgorithms true to enable legacy wrapping algorithms
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -967,12 +951,10 @@ public Builder enableLegacyWrappingAlgorithms(boolean shouldEnableLegacyWrapping
     
             //= specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes
             //# The S3EC MUST support the option to enable or disable legacy unauthenticated modes (content encryption algorithms).
    -
             /**
              * When set to true, decryption of content using legacy encryption algorithms
              * is enabled. This includes use of GetObject requests with a range, as this
              * mode is not authenticated.
    -         *
              * @param shouldEnableLegacyUnauthenticatedModes true to enable legacy content algorithms
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -983,15 +965,13 @@ public Builder enableLegacyUnauthenticatedModes(boolean shouldEnableLegacyUnauth
     
             //= specification/s3-encryption/client.md#enable-delayed-authentication
             //# The S3EC MUST support the option to enable or disable Delayed Authentication mode.
    -
             /**
              * When set to true, authentication of streamed objects is delayed until the
              * entire object is read from the stream. When this mode is enabled, the consuming
              * application must support a way to invalidate any data read from the stream as
              * the tag will not be validated until the stream is read to completion, as the
              * integrity of the data cannot be ensured. See the AWS Documentation for more
              * information.
    -         *
              * @param shouldEnableDelayedAuthenticationMode true to enable delayed authentication
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -1003,7 +983,6 @@ public Builder enableDelayedAuthenticationMode(boolean shouldEnableDelayedAuthen
             /**
              * When set to true, the putObject method will use multipart upload to perform
              * the upload. Disabled by default.
    -         *
              * @param _enableMultipartPutObject true enables the multipart upload implementation of putObject
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -1014,11 +993,9 @@ public Builder enableMultipartPutObject(boolean _enableMultipartPutObject) {
     
             //= specification/s3-encryption/client.md#set-buffer-size
             //# The S3EC SHOULD accept a configurable buffer size which refers to the maximum ciphertext length in bytes to store in memory when Delayed Authentication mode is disabled.
    -
             /**
              * Sets the buffer size for safe authentication used when delayed authentication mode is disabled.
              * If buffer size is not given during client configuration, default buffer size is set to 64MiB.
    -         *
              * @param bufferSize the desired buffer size in Bytes.
              * @return Returns a reference to this object so that method calls can be chained together.
              * @throws S3EncryptionClientException if the specified buffer size is outside the allowed bounds
    @@ -1041,7 +1018,6 @@ public Builder setBufferSize(long bufferSize) {
              * operations may fail.
              * Advanced option. Users who configure a {@link Provider} are responsible
              * for the security and correctness of the provider.
    -         *
              * @param cryptoProvider the {@link Provider to always use}
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -1052,12 +1028,10 @@ public Builder cryptoProvider(Provider cryptoProvider) {
     
             //= specification/s3-encryption/client.md#randomness
             //# The S3EC MAY accept a source of randomness during client initialization.
    -
             /**
              * Allows the user to pass an instance of {@link SecureRandom} to be used
              * for generating keys and IVs. Advanced option. Users who provide a {@link SecureRandom}
              * are responsible for the security and correctness of the {@link SecureRandom} implementation.
    -         *
              * @param secureRandom the {@link SecureRandom} instance to use
              * @return Returns a reference to this object so that method calls can be chained together.
              */
    @@ -1073,12 +1047,10 @@ public Builder secureRandom(SecureRandom secureRandom) {
             //# If the S3EC in a given language supports Instruction Files, then it MUST accept Instruction File Configuration during its initialization.
             //= specification/s3-encryption/client.md#instruction-file-configuration
             //# The S3EC MAY support the option to provide Instruction File Configuration during its initialization.
    -
             /**
              * Sets the Instruction File configuration for the S3 Encryption Client.
              * The InstructionFileConfig can be used to specify an S3 client to use for retrieval of Instruction files,
              * or to disable GetObject requests for the instruction file.
    -         *
              * @param instructionFileConfig
              * @return
              */
    @@ -1105,7 +1077,6 @@ public Builder encryptionAlgorithm(AlgorithmSuite encryptionAlgorithm) {
     
             //= specification/s3-encryption/client.md#key-commitment
             //# The S3EC MUST support configuration of the [Key Commitment policy](./key-commitment.md) during its initialization.
    -
             /**
              * Sets the commitment policy for this S3 encryption client.
              * The commitment policy determines whether the client requires, forbids, or allows
    @@ -1127,11 +1098,9 @@ public Builder commitmentPolicy(CommitmentPolicy commitmentPolicy) {
             //# The S3EC MAY support directly configuring the wrapped SDK clients through its initialization.
             //= specification/s3-encryption/client.md#inherited-sdk-configuration
             //# For example, the S3EC MAY accept a credentials provider instance during its initialization.
    -
             /**
              * The credentials provider to use for all inner clients, including KMS, if a KMS key ID is provided.
              * Note that if a wrapped client is configured, the wrapped client will take precedence over this option.
    -         *
              * @param awsCredentialsProvider
              * @return
              */
    @@ -1143,7 +1112,6 @@ public Builder credentialsProvider(AwsCredentialsProvider awsCredentialsProvider
     
             /**
              * The AWS region to use for all inner clients, including KMS, if a KMS key ID is provided.
    -         *
              * @param region
              * @return
              */
    @@ -1389,14 +1357,13 @@ public Builder asyncHttpClientBuilder(SdkAsyncHttpClient.Builder asyncHttpClient
             /**
              * Validates and builds the S3EncryptionClient according
              * to the configuration options passed to the Builder object.
    -         *
              * @return an instance of the S3EncryptionClient
              */
             public S3EncryptionClient build() {
                 if (!onlyOneNonNull(_cryptoMaterialsManager, _keyring, _aesKey, _rsaKeyPair, _kmsKeyId)) {
                     throw new S3EncryptionClientException("Exactly one must be set of: crypto materials manager, keyring, AES key, RSA key pair, KMS key id");
                 }
    -            if (_enableLegacyWrappingAlgorithms && _keyring != null) {
    +            if (_enableLegacyWrappingAlgorithms && _keyring !=null) {
                     S3Keyring keyring = (S3Keyring) _keyring;
                     if (!keyring.areLegacyWrappingAlgorithmsEnabled()) {
                         LogFactory.getLog(getClass()).warn("enableLegacyWrappingAlgorithms is set on the client, but is not set on the keyring provided. In order to enable legacy wrapping algorithms, set enableLegacyWrappingAlgorithms to true in the keyring's builder.");
    @@ -1515,32 +1482,35 @@ public S3EncryptionClient build() {
                         .cryptoMaterialsManager(_cryptoMaterialsManager)
                         .secureRandom(_secureRandom)
                         .instructionFileConfig(_instructionFileConfig)
    +                    .encryptionAlgorithm(_encryptionAlgorithm)
                         .build();
     
                 //= specification/s3-encryption/client.md#encryption-algorithm
                 //# The S3EC MUST validate that the configured encryption algorithm is not legacy.
    +            if (_encryptionAlgorithm.isLegacy()) {
    +                //= specification/s3-encryption/client.md#encryption-algorithm
    +                //# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception.
    +                throw new S3EncryptionClientException("Encryption algorithm provided is LEGACY! Please specify a fully-supported encryption algorithm.");
    +            }
    +
                 //= specification/s3-encryption/client.md#key-commitment
                 //# The S3EC MUST validate the configured Encryption Algorithm against the provided key commitment policy.
    +            if (_commitmentPolicy.requiresEncrypt() && !_encryptionAlgorithm.isCommitting()) {
    +                //= specification/s3-encryption/client.md#key-commitment
    +                //# If the configured Encryption Algorithm is incompatible with the key commitment policy, then it MUST throw an exception.
    +                throw new S3EncryptionClientException("The commitment policy requires encryption with a committing algorithm suite, " +
    +                        "but the specified encryption algorithm does not support key commitment. " +
    +                        "Please configure a committing algorithm suite such as ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.");
    +            }
                 //= specification/s3-encryption/key-commitment.md#commitment-policy
                 //# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST NOT encrypt using an algorithm suite which supports key commitment.
    -            if (_encryptionAlgorithm == null || _commitmentPolicy == null) {
    -                throw new S3EncryptionClientException(
    -                        "Both encryption algorithm and commitment policy must be configured."
    -                );
    -            }
    -            if (!(_encryptionAlgorithm.id() == AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.id()
    -                    && _commitmentPolicy.equals(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT))) {
    -                //= specification/s3-encryption/client.md#encryption-algorithm
    -                //# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception.
    +            if (_commitmentPolicy.equals(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) && _encryptionAlgorithm.isCommitting()) {
                     //= specification/s3-encryption/client.md#key-commitment
                     //# If the configured Encryption Algorithm is incompatible with the key commitment policy, then it MUST throw an exception.
    -                throw new S3EncryptionClientException(
    -                        "This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF. "
    -                                + "Note that the Encryption Algorithm does NOT effect Decryption; See: https://docs.aws.amazon.com/amazon-s3-encryption-client/latest/developerguide/encryption-algorithms.html#decryption-modes. "
    -                                + "Please update your configuration. Provided algorithm: " + _encryptionAlgorithm.name() + " and commitment policy: " + _commitmentPolicy.name()
    -                );
    +                throw new S3EncryptionClientException("The commitment policy forbids encryption with committing algorithm suites, " +
    +                        "but the specified encryption algorithm supports key commitment. " +
    +                        "Please configure a non-committing algorithm suite such as ALG_AES_256_GCM_IV12_TAG16_NO_KDF or change the commitment policy.");
                 }
    -
                 return new S3EncryptionClient(this);
             }
     
    
  • src/test/java/software/amazon/encryption/s3/AdditionalDecryptionKeyMaterialTest.java+73 24 modified
    @@ -10,6 +10,7 @@
     import software.amazon.awssdk.services.s3.model.GetObjectRequest;
     import software.amazon.awssdk.services.s3.model.GetObjectResponse;
     import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.materials.AesKeyring;
     import software.amazon.encryption.s3.materials.MaterialsDescription;
     import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
    @@ -82,7 +83,9 @@ public void testAesKeyringWithNullAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -100,7 +103,9 @@ public void testAesKeyringWithNullAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -142,7 +147,9 @@ public void testAesKeyringWithEmptyAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -161,7 +168,9 @@ public void testAesKeyringWithEmptyAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -203,7 +212,9 @@ public void testAesKeyringWithSingletonAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -228,7 +239,9 @@ public void testAesKeyringWithSingletonAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -270,7 +283,9 @@ public void testAesKeyringWithMultipleAdditionalKeyMaterials() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -307,7 +322,9 @@ public void testAesKeyringWithMultipleAdditionalKeyMaterials() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -349,7 +366,9 @@ public void testAesKeyringWithNonMatchingAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -378,7 +397,9 @@ public void testAesKeyringWithNonMatchingAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -420,7 +441,9 @@ public void testAesKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -449,7 +472,9 @@ public void testAesKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -490,7 +515,9 @@ public void testRsaKeyringWithNullAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -511,7 +538,9 @@ public void testRsaKeyringWithNullAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -556,7 +585,9 @@ public void testRsaKeyringWithEmptyAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -578,7 +609,9 @@ public void testRsaKeyringWithEmptyAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -623,7 +656,9 @@ public void testRsaKeyringWithSingletonAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -654,7 +689,9 @@ public void testRsaKeyringWithSingletonAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -699,7 +736,9 @@ public void testRsaKeyringWithMultipleAdditionalKeyMaterials() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -745,7 +784,9 @@ public void testRsaKeyringWithMultipleAdditionalKeyMaterials() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -790,7 +831,9 @@ public void testRsaKeyringWithNonMatchingAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -825,7 +868,9 @@ public void testRsaKeyringWithNonMatchingAdditionalKeyMaterial() {
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    @@ -870,7 +915,9 @@ public void testRsaKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey
                     .build();
     
             // Create an S3 encryption client for encryption
    -        S3Client encryptionClient = S3EncryptionClient.builder()
    +        S3Client encryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(encryptionKeyring)
                     .build();
     
    @@ -905,7 +952,9 @@ public void testRsaKeyringWithNonMatchingAdditionalKeyMaterialAndWrongDefaultKey
                     .build();
     
             // Create an S3 encryption client for decryption
    -        S3Client decryptionClient = S3EncryptionClient.builder()
    +        S3Client decryptionClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(decryptionKeyring)
                     .build();
     
    
  • src/test/java/software/amazon/encryption/s3/internal/CipherProviderTest.java+195 3 modified
    @@ -5,9 +5,11 @@
     import static org.junit.jupiter.api.Assertions.assertArrayEquals;
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertNotNull;
    +import static org.junit.jupiter.api.Assertions.assertNull;
     import static org.junit.jupiter.api.Assertions.assertThrows;
     import static org.junit.jupiter.api.Assertions.assertTrue;
     
    +import java.security.MessageDigest;
     import java.security.NoSuchAlgorithmException;
     import java.security.Provider;
     import java.security.SecureRandom;
    @@ -73,6 +75,24 @@ private DecryptionMaterials createDecryptionMaterials(AlgorithmSuite algorithmSu
                     .build();
         }
     
    +    @Test
    +    public void testCreateAndInitCipherWithCommittingAlgorithmValidMessageId() throws Exception {
    +        //= specification/s3-encryption/encryption.md#cipher-initialization
    +        //= type=test
    +        //# The client SHOULD validate that the generated IV or Message ID is not zeros.
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()]; // 224 bits / 8 = 28 bytes
    +        secureRandom.nextBytes(messageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG.clone(); // IV for committing algorithm
    +
    +        Cipher cipher = CipherProvider.createAndInitCipher(materials, iv, messageId);
    +
    +        assertNotNull(cipher);
    +        assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm());
    +    }
    +
         @Test
         public void testCreateAndInitCipherWithCommittingAlgorithmZeroMessageId() throws Exception {
             //= specification/s3-encryption/encryption.md#cipher-initialization
    @@ -124,6 +144,32 @@ public void testCreateAndInitCipherWithNonCommittingAlgorithmZeroIV() throws Exc
             assertEquals("IV has not been initialized!", exception.getMessage());
         }
     
    +    @Test
    +    public void testCreateAndInitCipherALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY() throws Exception {
    +        //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
    +        //= type=test
    +        //# The client MUST use HKDF to derive the key commitment value and the derived encrypting key
    +        //# as described in [Key Derivation](key-derivation.md).
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +        assertNull(materials.getKeyCommitment());
    +
    +        byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()];
    +        secureRandom.nextBytes(messageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG.clone();
    +
    +        materials.setIvAndMessageId(iv, messageId);
    +        Cipher cipher = materials.getCipher(iv);
    +
    +        assertNotNull(cipher);
    +        assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm());
    +
    +        // Verify that key commitment was set on encryption materials
    +        //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
    +        //= type=test
    +        //# The derived key commitment value MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        assertNotNull(materials.getKeyCommitment());
    +    }
     
         @Test
         public void testCreateAndInitCipherALG_AES_256_GCM_IV12_TAG16_NO_KDF() throws Exception {
    @@ -259,6 +305,24 @@ public void testCreateAndInitCipherALG_AES_256_CBC_IV16_NO_KDF_DecryptionSucceed
     
         @Test
         public void testKeyDerivationInputKeyMaterialLengthValidation() throws Exception {
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# - The length of the input keying material MUST equal the key derivation input length specified by the
    +        //# algorithm suite commit key derivation setting.
    +        SecretKey wrongSizeDataKey = createTestDataKey(16); // Wrong size - should be 32 bytes
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, wrongSizeDataKey);
    +
    +        byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()];
    +        secureRandom.nextBytes(messageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG.clone();
    +
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> CipherProvider.createAndInitCipher(materials, iv, messageId));
    +
    +        assertEquals("Length of Input key material does not match the expected value!", exception.getMessage());
    +    }
    +
    +    @Test
    +    public void testKeyDerivationInputKeyMaterialLengthValidationDecryptionMaterials() throws Exception {
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
             //= type=test
             //# - The length of the input keying material MUST equal the key derivation input length specified by the
    @@ -277,6 +341,23 @@ public void testKeyDerivationInputKeyMaterialLengthValidation() throws Exception
     
         @Test
         public void testKeyDerivationMessageIdLengthValidation() throws Exception {
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# - The salt MUST be the Message ID with the length defined in the algorithm suite.
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] wrongSizeMessageId = new byte[16]; // Wrong size - should be 28 bytes
    +        secureRandom.nextBytes(wrongSizeMessageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG.clone();
    +
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> CipherProvider.createAndInitCipher(materials, iv, wrongSizeMessageId));
    +        
    +        assertEquals("Length of Input Message ID does not match the expected value!", exception.getMessage());
    +    }
    +
    +    @Test
    +    public void testKeyDerivationMessageIdLengthValidationDecryptionMaterials() throws Exception {
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
             //= type=test
             //# - The salt MUST be the Message ID with the length defined in the algorithm suite.
    @@ -292,6 +373,117 @@ public void testKeyDerivationMessageIdLengthValidation() throws Exception {
             assertEquals("Length of Input Message ID does not match the expected value!", exception.getMessage());
         }
     
    +    @Test
    +    public void testKeyCommitmentVerification() throws Exception {
    +        SecretKey dataKey = createTestDataKey(32);
    +
    +        // First, create encryption materials to generate the key commitment
    +        EncryptionMaterials encMaterials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] messageId = new byte[encMaterials.algorithmSuite().commitmentNonceLengthBytes()];
    +        secureRandom.nextBytes(messageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG.clone();
    +
    +        // Generate the cipher and key commitment
    +        assertNull(encMaterials.getKeyCommitment());
    +        Cipher encCipher = CipherProvider.createAndInitCipher(encMaterials, iv, messageId);
    +
    +        // Verify that key commitment was set
    +        assertNotNull(encMaterials.getKeyCommitment());
    +
    +        // Create decryption materials with the same key commitment
    +        DecryptionMaterials decMaterials = createDecryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey, encMaterials.getKeyCommitment());
    +        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +        //= type=test
    +        //# When using an algorithm suite which supports key commitment, the client MUST verify that the
    +        //# [derived key commitment](./key-derivation.md#hkdf-operation) contains the same bytes as the stored key
    +        //# commitment retrieved from the stored object's metadata.
    +        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +        //= type=test
    +        //# When using an algorithm suite which supports key commitment, the client MUST verify the key commitment values match
    +        //# before deriving the [derived encryption key](./key-derivation.md#hkdf-operation).
    +        Cipher decCipher = CipherProvider.createAndInitCipher(decMaterials, FIXED_IV_FOR_COMMIT_ALG.clone(), messageId);
    +        assertNotNull(decCipher.getAlgorithm());
    +
    +        // Create decryption materials with the wrong key commitment
    +        byte[] wrongKeyCommitment = new byte[28];
    +        secureRandom.nextBytes(wrongKeyCommitment);
    +        DecryptionMaterials decMaterialsWithWrongKeyCommitment = createDecryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey, wrongKeyCommitment);
    +        //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    +        //= type=test
    +        //# When using an algorithm suite which supports key commitment, the client MUST throw an exception when the
    +        //# derived key commitment value and stored key commitment value do not match.
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> CipherProvider.createAndInitCipher(decMaterialsWithWrongKeyCommitment, iv, messageId));
    +        assertEquals(S3EncryptionClientSecurityException.class, exception.getCause().getClass());
    +        assertEquals("Key commitment validation failed. The derived key commitment does not match the stored key commitment value. " +
    +                        "This indicates potential data tampering or corruption.",
    +                exception.getCause().getMessage());
    +    }
    +
    +    @Test
    +    public void testKeyDerivationWithZeroIVForCommittingAlgorithm() throws Exception {
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=TODO
    +        //# When encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    +        //# the IV used in the AES-GCM content encryption/decryption MUST consist entirely of bytes with the value 0x01.
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] messageId = new byte[28];
    +        secureRandom.nextBytes(messageId);
    +
    +        Cipher cipher = CipherProvider.createAndInitCipher(materials, FIXED_IV_FOR_COMMIT_ALG.clone(), messageId);
    +
    +        assertNotNull(cipher);
    +
    +        // Verify the cipher was initialized with zero IV
    +        GCMParameterSpec params = cipher.getParameters().getParameterSpec(GCMParameterSpec.class);
    +        byte[] actualIV = params.getIV();
    +        assertTrue(MessageDigest.isEqual(FIXED_IV_FOR_COMMIT_ALG.clone(), actualIV));
    +    }
    +
    +    @Test
    +    public void testGenerateDerivedEncryptionKey() throws Exception {
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] messageId = new byte[materials.algorithmSuite().commitmentNonceLengthBytes()];
    +        secureRandom.nextBytes(messageId);
    +
    +        assertNull(materials.getKeyCommitment());
    +        SecretKey ek = CipherProvider.generateDerivedEncryptionKey(materials, messageId);
    +        assertNotNull(ek);
    +        assertNotNull(materials.getKeyCommitment());
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# - The length of the output keying material MUST equal the commit key length specified by the supported algorithm suites.
    +        assertEquals(materials.algorithmSuite().commitmentLengthBytes(), materials.getKeyCommitment().length);
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# - The length of the output keying material MUST equal the encryption key length specified by the algorithm suite encryption settings.
    +        assertEquals(materials.algorithmSuite().dataKeyLengthBytes(), ek.getEncoded().length);
    +    }
    +
    +    @Test
    +    public void testKeyDerivationHashAlgorithmFromAlgorithmSuite() throws Exception {
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# - The hash function MUST be specified by the algorithm suite commitment settings.
    +        SecretKey dataKey = createTestDataKey(32);
    +        EncryptionMaterials materials = createEncryptionMaterials(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, dataKey);
    +
    +        byte[] messageId = new byte[28];
    +        secureRandom.nextBytes(messageId);
    +        byte[] iv = FIXED_IV_FOR_COMMIT_ALG;
    +
    +        // This test verifies that the correct hash algorithm (HmacSHA512) is used
    +        // The test passes if no exception is thrown during cipher creation
    +        Cipher cipher = CipherProvider.createAndInitCipher(materials, iv, messageId);
    +
    +        assertNotNull(cipher);
    +        assertEquals("AES/GCM/NoPadding", cipher.getAlgorithm());
    +    }
    +
         @Test
         public void testKeyCommitmentValidationBothSuccessAndFailurePaths() throws Exception {
             //= specification/s3-encryption/decryption.md#decrypting-with-commitment
    @@ -335,9 +527,9 @@ public void testKeyCommitmentValidationBothSuccessAndFailurePaths() throws Excep
     
             // Step 3: Create decryption materials with the same secret key and correct key commitment
             DecryptionMaterials correctMaterials = createDecryptionMaterials(
    -            AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    -            dataKey,
    -            correctKeyCommitment
    +                AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    +                dataKey,
    +                correctKeyCommitment
             );
     
             //= specification/s3-encryption/key-derivation.md#hkdf-operation
    
  • src/test/java/software/amazon/encryption/s3/internal/CipherSubscriberTest.java+20 8 modified
    @@ -24,6 +24,14 @@
     import static org.junit.jupiter.api.Assertions.assertTrue;
     
     class CipherSubscriberTest {
    +
    +    // Use a common static IV
    +    private static final byte[] _iv = new byte[12];
    +
    +    static {
    +        _iv[0] = 1;
    +    }
    +
         // Helper classes for testing
         class SimpleSubscriber implements Subscriber<ByteBuffer> {
     
    @@ -139,11 +147,18 @@ private EncryptionMaterials getTestEncryptMaterials(String plaintext) {
                 KeyGenerator keyGen = KeyGenerator.getInstance("AES");
                 keyGen.init(256);
                 AES_KEY = keyGen.generateKey();
    -            return EncryptionMaterials.builder()
    +            EncryptionMaterials materials = EncryptionMaterials.builder()
                         .plaintextDataKey(AES_KEY.getEncoded())
                         .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                         .plaintextLength(plaintext.getBytes(StandardCharsets.UTF_8).length)
                         .build();
    +            if (materials.algorithmSuite().isCommitting()) {
    +                // Set MessageId or IV
    +                materials.setIvAndMessageId(new byte[12], _iv);
    +            } else {
    +                materials.setIvAndMessageId(_iv, null);
    +            }
    +            return materials;
             } catch (NoSuchAlgorithmException exception) {
                 // this should never happen
                 throw new RuntimeException("AES doesn't exist");
    @@ -181,11 +196,8 @@ public void testSubscriberBehaviorOneChunk() {
             AlgorithmSuite algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
             String plaintext = "unit test of cipher subscriber";
             EncryptionMaterials materials = getTestEncryptMaterials(plaintext);
    -        byte[] iv = new byte[materials.algorithmSuite().iVLengthBytes()];
    -        // we reject 0-ized IVs, so just do something
    -        iv[0] = 1;
             SimpleSubscriber wrappedSubscriber = new SimpleSubscriber();
    -        CipherSubscriber subscriber = new CipherSubscriber(wrappedSubscriber, materials.getCiphertextLength(), materials, iv, null);
    +        CipherSubscriber subscriber = new CipherSubscriber(wrappedSubscriber, materials.getCiphertextLength(), materials, materials.iv(), materials.messageId());
     
             // Act
             TestPublisher<ByteBuffer> publisher = new TestPublisher<>();
    @@ -208,7 +220,7 @@ public void testSubscriberBehaviorOneChunk() {
             // Now decrypt.
             DecryptionMaterials decryptionMaterials = getTestDecryptionMaterialsFromEncMats(materials);
             SimpleSubscriber wrappedDecryptSubscriber = new SimpleSubscriber();
    -        CipherSubscriber decryptSubscriber = new CipherSubscriber(wrappedDecryptSubscriber, expectedLength, decryptionMaterials, iv, null);
    +        CipherSubscriber decryptSubscriber = new CipherSubscriber(wrappedDecryptSubscriber, expectedLength, decryptionMaterials, materials.iv(), materials.messageId());
             TestPublisher<ByteBuffer> decryptPublisher = new TestPublisher<>();
             decryptPublisher.subscribe(decryptSubscriber);
     
    @@ -239,7 +251,7 @@ public void testSubscriberBehaviorTagLengthLastChunk() {
             // we reject 0-ized IVs, so just do something non-zero
             iv[0] = 1;
             SimpleSubscriber wrappedSubscriber = new SimpleSubscriber();
    -        CipherSubscriber subscriber = new CipherSubscriber(wrappedSubscriber, materials.getCiphertextLength(), materials, iv, null);
    +        CipherSubscriber subscriber = new CipherSubscriber(wrappedSubscriber, materials.getCiphertextLength(), materials, iv, materials.messageId());
     
             // Setup Publisher
             TestPublisher<ByteBuffer> publisher = new TestPublisher<>();
    @@ -262,7 +274,7 @@ public void testSubscriberBehaviorTagLengthLastChunk() {
             // Now decrypt the ciphertext
             DecryptionMaterials decryptionMaterials = getTestDecryptionMaterialsFromEncMats(materials);
             SimpleSubscriber wrappedDecryptSubscriber = new SimpleSubscriber();
    -        CipherSubscriber decryptSubscriber = new CipherSubscriber(wrappedDecryptSubscriber, expectedLength, decryptionMaterials, iv, null);
    +        CipherSubscriber decryptSubscriber = new CipherSubscriber(wrappedDecryptSubscriber, expectedLength, decryptionMaterials, iv, materials.messageId());
             TestPublisher<ByteBuffer> decryptPublisher = new TestPublisher<>();
             decryptPublisher.subscribe(decryptSubscriber);
     
    
  • src/test/java/software/amazon/encryption/s3/internal/ContentMetadataStrategyTest.java+155 0 modified
    @@ -783,4 +783,159 @@ private String metadataToString(Map<String, String> metadataMap) {
                 throw new S3EncryptionClientException("Cannot serialize materials to JSON.", e);
             }
         }
    +
    +    @Test
    +    public void testEncodeMetaV3WithAESGCM() {
    +        // Test V3 encoding with AES/GCM wrapping algorithm
    +        EncryptedDataKey edk = EncryptedDataKey.builder()
    +                .encryptedDataKey("encrypted-key-data".getBytes(StandardCharsets.UTF_8))
    +                .keyProviderId("test-provider")
    +                .keyProviderInfo("AES/GCM")
    +                .build();
    +
    +        MaterialsDescription materialsDescription = MaterialsDescription.builder()
    +                .put("test", "material-desc")
    +                .put("custom", "value")
    +                .build();
    +
    +        EncryptionMaterials materials = EncryptionMaterials.builder()
    +                .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .encryptedDataKeys(java.util.Collections.singletonList(edk))
    +                .materialsDescription(materialsDescription)
    +                .build();
    +
    +        // Key Commitment is set during Cipher Initialization
    +        materials.setKeyCommitment("key-commitment-data".getBytes(StandardCharsets.UTF_8));
    +
    +        byte[] iv = "test-iv-28-bytes-long-1234567890".getBytes(StandardCharsets.UTF_8);
    +
    +        PutObjectRequest originalRequest = PutObjectRequest.builder()
    +                .bucket("test-bucket")
    +                .key("test-key")
    +                .build();
    +
    +        PutObjectRequest result = encodingStrategy.encodeMetadata(materials, iv, originalRequest);
    +
    +        // Verify V3 format metadata
    +        Map<String, String> metadata = result.metadata();
    +        assertNotNull(metadata);
    +
    +        assertEquals(Base64.getEncoder().encodeToString(edk.encryptedDatakey()),
    +                metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3));
    +        assertEquals(Base64.getEncoder().encodeToString(iv),
    +                metadata.get(MetadataKeyConstants.MESSAGE_ID_V3));
    +        assertEquals("115",
    +                metadata.get(MetadataKeyConstants.CONTENT_CIPHER_V3));
    +        //= specification/s3-encryption/data-format/content-metadata.md#v3-only
    +        //= type=test
    +        //# The Material Description MUST be used for wrapping algorithms `AES/GCM` (`02`) and `RSA-OAEP-SHA1` (`22`).
    +        assertEquals("02", // Compressed AES/GCM
    +                metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3));
    +        assertEquals(Base64.getEncoder().encodeToString(materials.getKeyCommitment()),
    +                metadata.get(MetadataKeyConstants.KEY_COMMITMENT_V3));
    +
    +        //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
    +        //= type=test
    +        //# - The mapkey "x-amz-m" SHOULD be present for V3 format objects that use Raw Keyring Material Description.
    +        String matDesc = metadata.get(MetadataKeyConstants.MAT_DESC_V3);
    +        assertNotNull(matDesc);
    +        assertTrue(matDesc.contains("test"));
    +        assertTrue(matDesc.contains("material-desc"));
    +        assertTrue(matDesc.contains("custom"));
    +        assertTrue(matDesc.contains("value"));
    +    }
    +
    +    @Test
    +    public void testEncodeMetaV3WithKMSContext() {
    +        // Test V3 encoding with kms+context wrapping algorithm
    +        EncryptedDataKey edk = EncryptedDataKey.builder()
    +                .encryptedDataKey("encrypted-key-data".getBytes(StandardCharsets.UTF_8))
    +                .keyProviderId("test-provider")
    +                .keyProviderInfo("kms+context")
    +                .build();
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("kms_cmk_id", "test-key-id");
    +        encryptionContext.put("custom", "value");
    +
    +        EncryptionMaterials materials = EncryptionMaterials.builder()
    +                .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .encryptedDataKeys(java.util.Collections.singletonList(edk))
    +                .encryptionContext(encryptionContext)
    +                .build();
    +
    +        materials.setKeyCommitment("key-commitment-data".getBytes(StandardCharsets.UTF_8));
    +
    +        byte[] iv = "test-iv-28-bytes-long-1234567890".getBytes(StandardCharsets.UTF_8);
    +
    +        PutObjectRequest originalRequest = PutObjectRequest.builder()
    +                .bucket("test-bucket")
    +                .key("test-key")
    +                .build();
    +
    +        PutObjectRequest result = encodingStrategy.encodeMetadata(materials, iv, originalRequest);
    +
    +        Map<String, String> metadata = result.metadata();
    +        //= specification/s3-encryption/data-format/content-metadata.md#v3-only
    +        //= type=test
    +        //# The Encryption Context value MUST be used for wrapping algorithm `kms+context` or `12`.
    +        assertEquals("12", // Compressed kms+context
    +                metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3));
    +
    +        //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
    +        //= type=test
    +        //# - The mapkey "x-amz-t" SHOULD be present for V3 format objects that use KMS Encryption Context.
    +        String encCtx = metadata.get(MetadataKeyConstants.ENCRYPTION_CONTEXT_V3);
    +        assertNotNull(encCtx);
    +        assertTrue(encCtx.contains("kms_cmk_id"));
    +        assertTrue(encCtx.contains("test-key-id"));
    +        assertTrue(encCtx.contains("custom"));
    +        assertTrue(encCtx.contains("value"));
    +    }
    +
    +    @Test
    +    public void testEncodeMetaV3WithRSAOAEP() {
    +        // Test V3 encoding with RSA-OAEP-SHA1 wrapping algorithm
    +        EncryptedDataKey edk = EncryptedDataKey.builder()
    +                .encryptedDataKey("encrypted-key-data".getBytes(StandardCharsets.UTF_8))
    +                .keyProviderId("test-provider")
    +                .keyProviderInfo("RSA-OAEP-SHA1")
    +                .build();
    +
    +        MaterialsDescription materialsDescription = MaterialsDescription.builder()
    +                .put("rsa", "material-desc")
    +                .build();
    +
    +        EncryptionMaterials materials = EncryptionMaterials.builder()
    +                .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .encryptedDataKeys(java.util.Collections.singletonList(edk))
    +                .materialsDescription(materialsDescription)
    +                .build();
    +
    +        materials.setKeyCommitment("key-commitment-data".getBytes(StandardCharsets.UTF_8));
    +
    +        byte[] iv = "test-iv-28-bytes-long-1234567890".getBytes(StandardCharsets.UTF_8);
    +
    +        PutObjectRequest originalRequest = PutObjectRequest.builder()
    +                .bucket("test-bucket")
    +                .key("test-key")
    +                .build();
    +
    +        PutObjectRequest result = encodingStrategy.encodeMetadata(materials, iv, originalRequest);
    +
    +        Map<String, String> metadata = result.metadata();
    +        //= specification/s3-encryption/data-format/content-metadata.md#v3-only
    +        //= type=test
    +        //# The Material Description MUST be used for wrapping algorithms `AES/GCM` (`02`) and `RSA-OAEP-SHA1` (`22`).
    +        assertEquals("22", // Compressed RSA-OAEP-SHA1
    +                metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3));
    +
    +        //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
    +        //= type=test
    +        //# - The mapkey "x-amz-m" SHOULD be present for V3 format objects that use Raw Keyring Material Description.
    +        String matDesc = metadata.get(MetadataKeyConstants.MAT_DESC_V3);
    +        assertNotNull(matDesc);
    +        assertTrue(matDesc.contains("rsa"));
    +        assertTrue(matDesc.contains("material-desc"));
    +    }
     }
    
  • src/test/java/software/amazon/encryption/s3/internal/StreamingAesGcmContentStrategyTest.java+60 1 modified
    @@ -8,6 +8,7 @@
     import static org.junit.jupiter.api.Assertions.assertThrows;
     import static org.junit.jupiter.api.Assertions.assertTrue;
     
    +import javax.crypto.Cipher;
     import javax.crypto.KeyGenerator;
     import javax.crypto.SecretKey;
     
    @@ -19,6 +20,9 @@
     import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.materials.EncryptionMaterials;
     
    +import java.security.MessageDigest;
    +import java.util.Arrays;
    +
     public class StreamingAesGcmContentStrategyTest {
     
         private static SecretKey AES_KEY;
    @@ -97,10 +101,65 @@ public void testEncryptContentWithNonCommitingAlgorithm() {
             //= type=test
             //# The generated IV or Message ID MUST be set or returned from the encryption process such that it can be included in the content metadata.
             assertNotNull(encryptContent.iv());
    -        assertNull(encryptContent.messageId());
    +        assertNotNull(encryptContent.messageId());
    +        assertNotNull(materials.iv());
    +        assertNotNull(materials.messageId());
             //= specification/s3-encryption/encryption.md#content-encryption
             //= type=test
             //# The client MUST generate an IV or Message ID using the length of the IV or Message ID defined in the algorithm suite.
             assertEquals(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.iVLengthBytes(), encryptContent.iv().length);
    +        assertEquals(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.commitmentNonceLengthBytes(), encryptContent.messageId().length);
    +    }
    +
    +    @Test
    +    public void testEncryptContentWithCommittingAlgorithm() {
    +        StreamingAesGcmContentStrategy strategy = StreamingAesGcmContentStrategy.builder().build();
    +
    +        // Committing algorithm
    +        EncryptionMaterials materials = EncryptionMaterials.builder()
    +                .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .plaintextDataKey(AES_KEY.getEncoded())
    +                .plaintextLength(100L)
    +                .build();
    +
    +        AsyncRequestBody content = AsyncRequestBody.fromString("test");
    +
    +        // Ensure Materials IV and Null are set to Null before generating.
    +        assertNull(materials.iv());
    +        assertNull(materials.messageId());
    +
    +        EncryptedContent encryptContent = strategy.encryptContent(materials, content);
    +        //= specification/s3-encryption/encryption.md#content-encryption
    +        //= type=test
    +        //# The generated IV or Message ID MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        assertNotNull(encryptContent.iv());
    +        assertNotNull(encryptContent.messageId());
    +        assertNotNull(materials.iv());
    +        assertNotNull(materials.messageId());
    +        //= specification/s3-encryption/encryption.md#content-encryption
    +        //= type=test
    +        //# The client MUST generate an IV or Message ID using the length of the IV or Message ID defined in the algorithm suite.
    +        assertEquals(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.iVLengthBytes(), encryptContent.iv().length);
    +        assertEquals(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.commitmentNonceLengthBytes(), encryptContent.messageId().length);
    +        // Verify that key commitment was set on encryption materials
    +        //= specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
    +        //= type=test
    +        //# The derived key commitment value MUST be set or returned from the encryption process such that it can be included in the content metadata.
    +        assertNotNull(materials.getKeyCommitment());
    +        assertEquals(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.commitmentLengthBytes(), materials.getKeyCommitment().length);
    +        byte[] iv = new byte[12];
    +        Arrays.fill(iv, (byte) 0x01);
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# When encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
    +        //# the IV used in the AES-GCM content encryption/decryption MUST consist entirely of bytes with the value 0x01.
    +        assertTrue(MessageDigest.isEqual(iv, encryptContent.iv()));
    +        //= specification/s3-encryption/key-derivation.md#hkdf-operation
    +        //= type=test
    +        //# The client MUST initialize the cipher, or call an AES-GCM encryption API, with the derived encryption key, an IV containing only bytes with the value 0x01,
    +        //# and the tag length defined in the Algorithm Suite when encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.
    +        Cipher cipher = materials.getCipher(materials.iv());
    +        assertNotNull(cipher);
    +        assertTrue(MessageDigest.isEqual(iv, cipher.getIV()));
         }
     }
    
  • src/test/java/software/amazon/encryption/s3/materials/EncryptedDataKeyTest.java+1 1 modified
    @@ -15,7 +15,7 @@ public class EncryptedDataKeyTest {
         private byte[] encryptedDataKey;
         private String keyProviderId;
         private String keyProviderInfo;
    -    
    +
         @BeforeEach
         public void setUp() {
             keyProviderId = "testKeyProviderId";
    
  • src/test/java/software/amazon/encryption/s3/materials/EncryptionMaterialsTest.java+20 0 modified
    @@ -8,10 +8,14 @@
     import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.internal.CipherMode;
     
    +import java.security.MessageDigest;
    +import java.security.SecureRandom;
     import java.util.*;
     
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertNotEquals;
    +import static org.junit.jupiter.api.Assertions.assertNull;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
     
     class EncryptionMaterialsTest {
     
    @@ -67,6 +71,22 @@ void testCipherMode() {
             assertEquals(CipherMode.ENCRYPT, actualEncryptionMaterials.cipherMode());
         }
     
    +    @Test
    +    void testSetIvAndMessageId() {
    +        SecureRandom secureRandom = new SecureRandom();
    +        byte[] iv = new byte[12];
    +        secureRandom.nextBytes(iv);
    +        byte[] messageId = new byte[28];
    +        secureRandom.nextBytes(messageId);
    +
    +        assertNull(actualEncryptionMaterials.iv());
    +        assertNull(actualEncryptionMaterials.messageId());
    +
    +        actualEncryptionMaterials.setIvAndMessageId(iv, messageId);
    +        assertTrue(MessageDigest.isEqual(iv, actualEncryptionMaterials.iv()));
    +        assertTrue(MessageDigest.isEqual(messageId, actualEncryptionMaterials.messageId()));
    +    }
    +
         @Test
         void testToBuilder() {
             EncryptionMaterials actualToBuilder = actualEncryptionMaterials.toBuilder().build();
    
  • src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java+2 15 modified
    @@ -19,7 +19,6 @@
     import software.amazon.encryption.s3.CommitmentPolicy;
     import software.amazon.encryption.s3.S3EncryptionClient;
     import software.amazon.encryption.s3.S3EncryptionClientException;
    -import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     
     import java.io.ByteArrayInputStream;
     import java.nio.charset.StandardCharsets;
    @@ -69,9 +68,8 @@ public void testKmsDiscovery() {
             KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring
               .builder().enableLegacyWrappingAlgorithms(true).build();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(kmsDiscoveryKeyring)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
                     .build();
     
             // Asserts
    @@ -104,9 +102,8 @@ public void testKmsContextV2Discovery() {
             KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring
               .builder().enableLegacyWrappingAlgorithms(true).build();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .keyring(kmsDiscoveryKeyring)
    +          .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
               .build();
     
             // Asserts
    @@ -139,8 +136,6 @@ public void testKmsContextV3Discovery() {
     
             // V3 Client - KmsKeyring
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .kmsKeyId(KMS_KEY_ID)
               .build();
     
    @@ -157,8 +152,6 @@ public void testKmsContextV3Discovery() {
             KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring
               .builder().enableLegacyWrappingAlgorithms(true).build();
             S3Client s3ClientDiscovery = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .keyring(kmsDiscoveryKeyring)
               .build();
     
    @@ -180,8 +173,6 @@ public void testKmsContextV3DiscoveryWrongECFails() {
     
             // V3 Client - KmsKeyring
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .kmsKeyId(KMS_KEY_ID)
               .build();
     
    @@ -198,8 +189,6 @@ public void testKmsContextV3DiscoveryWrongECFails() {
             KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring
               .builder().enableLegacyWrappingAlgorithms(true).build();
             S3Client s3ClientDiscovery = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .keyring(kmsDiscoveryKeyring)
               .build();
     
    @@ -229,8 +218,6 @@ public void testKmsContextV3DiscoveryEncryptFails() {
             KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring
               .builder().enableLegacyWrappingAlgorithms(true).build();
             S3Client s3ClientDiscovery = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .keyring(kmsDiscoveryKeyring)
               .build();
     
    
  • src/test/java/software/amazon/encryption/s3/S3AsyncEncryptionClientTest.java+4 65 modified
    @@ -120,8 +120,6 @@ public void asyncCustomConfiguration() {
                     .wrappingKeyId(KMS_KEY_ID)
                     .build();
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedClient(wrappedAsyncClient)
                     .keyring(keyring)
                     .build();
    @@ -154,8 +152,6 @@ public void asyncTopLevelConfigurationAllOptions() {
             // use all top-level options;
             // there isn't a good way to validate every option.
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -204,8 +200,6 @@ public void asyncTopLevelConfiguration() {
             AwsCredentialsProvider creds = DefaultCredentialsProvider.create();
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -240,8 +234,6 @@ public void s3AsyncEncryptionClientTopLevelAlternateCredentials() {
             AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider();
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -264,8 +256,6 @@ public void s3AsyncEncryptionClientTopLevelAlternateCredentials() {
     
             // using the alternate key succeeds
             S3AsyncClient s3ClientAltCreds = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(ALTERNATE_KMS_KEY)
    @@ -307,8 +297,6 @@ public void s3AsyncEncryptionClientMixedCredentials() {
                     .build();
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .keyring(kmsKeyring)
    @@ -340,8 +328,6 @@ public void asyncTopLevelConfigurationWrongRegion() {
             AwsCredentialsProvider creds = DefaultCredentialsProvider.create();
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of("eu-west-1"))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -370,8 +356,6 @@ public void asyncTopLevelConfigurationNullCreds() {
             AwsCredentialsProvider creds = new S3EncryptionClientTestResources.NullCredentialsProvider();
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -398,14 +382,10 @@ public void putAsyncGetDefault() {
             final String objectKey = appendTestSuffix("put-async-get-default");
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -435,14 +415,10 @@ public void putDefaultGetAsync() {
             final String objectKey = appendTestSuffix("put-default-get-async");
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -472,8 +448,6 @@ public void putAsyncGetAsync() {
             final String objectKey = appendTestSuffix("put-async-get-async");
     
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -558,8 +532,6 @@ public void failAesCbcV1toV3AsyncWhenDisabled() {
     
             // V3 Client
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(true)
                     .build();
    @@ -633,8 +605,6 @@ public void deleteObjectWithInstructionFileSuccessAsync() {
     
             // V3 Client
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             final String input = "DeleteObjectWithInstructionFileSuccess";
    @@ -680,8 +650,6 @@ public void deleteObjectsWithInstructionFilesSuccessAsync() {
     
             // V3 Client
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             final String input = "DeleteObjectsWithInstructionFileSuccess";
    @@ -716,8 +684,6 @@ public void deleteObjectsWithInstructionFilesSuccessAsync() {
         public void deleteObjectWithWrongObjectKeySuccessAsync() {
             // V3 Client
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             assertDoesNotThrow(() -> s3Client.deleteObject(builder -> builder.bucket(BUCKET).key("InvalidKey")));
    @@ -732,8 +698,6 @@ public void copyObjectTransparentlyAsync() {
             final String newObjectKey = appendTestSuffix("copy-object-to-here-async");
     
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -775,7 +739,6 @@ public void copyObjectTransparentlyAsync() {
          * the cipher's block size.
          * Note that TinyAsyncRequestBody is not fully spec-compliant, and will cause IllegalStateExceptions
          * to be logged when debug logging is enabled.
    -     *
          * @throws IOException
          */
         @Test
    @@ -786,8 +749,6 @@ public void tinyBufferTest() throws IOException {
             final String objectKey = appendTestSuffix("tiny-buffer-async");
     
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     .build();
    @@ -880,7 +841,6 @@ public void testAsyncInstructionFileConfig() {
             s3ClientDisabledInstructionFile.close();
             s3Client.close();
         }
    -
         @Test
         public void testAsyncInstructionFileConfigMultipart() {
             final String objectKey = appendTestSuffix("test-multipart-async-instruction-file-config");
    @@ -889,8 +849,6 @@ public void testAsyncInstructionFileConfigMultipart() {
             AwsCredentialsProvider credentials = DefaultCredentialsProvider.create();
             S3Client wrappedClient = S3Client.create();
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -927,7 +885,6 @@ public void testAsyncInstructionFileConfigMultipart() {
     
             s3Client.close();
         }
    -
         @Test
         public void testAsyncInstructionFileConfigMultipartWithOptions() {
             final String objectKey = appendTestSuffix("test-multipart-async-instruction-file-config-options");
    @@ -936,8 +893,6 @@ public void testAsyncInstructionFileConfigMultipartWithOptions() {
     
             S3Client wrappedClient = S3Client.create();
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -990,8 +945,6 @@ public void wrappedClientMultipartUploadThrowsException() throws IOException {
             // using top-level configuration throws an exception
             try {
                 S3AsyncEncryptionClient.builderV4()
    -                    .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                    .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                         .kmsKeyId(KMS_KEY_ID)
                         .enableMultipartPutObject(true)
                         .multipartEnabled(true)
    @@ -1010,8 +963,6 @@ public void wrappedClientMultipartUploadThrowsException() throws IOException {
                     .multipartEnabled(true)
                     .build();
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .enableMultipartPutObject(true)
                     .wrappedClient(wrappedClient)
    @@ -1045,8 +996,6 @@ public void wrappedClientMultipartUploadThrowsException() throws IOException {
     
             // using a client without MPU should pass
             S3AsyncClient s3ClientGet = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .enableDelayedAuthenticationMode(true)
                     .cryptoProvider(PROVIDER)
    @@ -1067,26 +1016,20 @@ public void wrappedClientMultipartUploadThrowsException() throws IOException {
         @Test
         public void s3AsyncClientBuilderForbidsMultipartEnabled() {
             assertThrows(
    -                UnsupportedOperationException.class,
    -                () -> S3AsyncEncryptionClient.builderV4()
    -                        .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                        .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF).multipartEnabled(Boolean.TRUE));
    +            UnsupportedOperationException.class,
    +            () -> S3AsyncEncryptionClient.builderV4().multipartEnabled(Boolean.TRUE));
         }
     
         @Test
         public void s3AsyncClientBuilderForbidsMultipartConfiguration() {
             assertThrows(
    -                UnsupportedOperationException.class,
    -                () -> S3AsyncEncryptionClient.builderV4()
    -                        .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                        .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF).multipartConfiguration(MultipartConfiguration.builder().build()));
    +            UnsupportedOperationException.class,
    +            () -> S3AsyncEncryptionClient.builderV4().multipartConfiguration(MultipartConfiguration.builder().build()));
         }
     
         @Test
         public void s3AsyncClientForbidsCreateMultipartUpload() {
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId("fails")
                     .build();
     
    @@ -1097,8 +1040,6 @@ public void s3AsyncClientForbidsCreateMultipartUpload() {
         @Test
         public void s3AsyncClientForbidsUploadPart() {
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId("fails")
                     .build();
     
    @@ -1109,8 +1050,6 @@ public void s3AsyncClientForbidsUploadPart() {
         @Test
         public void s3AsyncClientForbidsCompleteMultipartUpload() {
             S3AsyncClient s3AsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId("fails")
                     .build();
     
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientBuilderValidationTest.java+15 50 modified
    @@ -33,8 +33,6 @@ public void testBuilderWithMultipleKeyringTypesFails() throws NoSuchAlgorithmExc
             // Test AES + RSA
             S3EncryptionClientException exception1 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .rsaKeyPair(rsaKeyPair)
                     .build()
    @@ -44,8 +42,6 @@ public void testBuilderWithMultipleKeyringTypesFails() throws NoSuchAlgorithmExc
             // Test AES + KMS
             S3EncryptionClientException exception2 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .kmsKeyId("test-key-id")
                     .build()
    @@ -55,8 +51,6 @@ public void testBuilderWithMultipleKeyringTypesFails() throws NoSuchAlgorithmExc
             // Test RSA + KMS
             S3EncryptionClientException exception3 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(rsaKeyPair)
                     .kmsKeyId("test-key-id")
                     .build()
    @@ -67,9 +61,7 @@ public void testBuilderWithMultipleKeyringTypesFails() throws NoSuchAlgorithmExc
         @Test
         public void testBuilderWithNoKeyringFails() {
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
    -            S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF).build()
    +            S3EncryptionClient.builderV4().build()
             );
             assertTrue(exception.getMessage().contains("Exactly one must be set of"));
         }
    @@ -87,8 +79,6 @@ public void testBuilderWithCMMAndKeyringFails() throws NoSuchAlgorithmException
     
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .cryptoMaterialsManager(cmm)
                     .build()
    @@ -105,8 +95,6 @@ public void testBuilderWithInvalidBufferSizes() throws NoSuchAlgorithmException
             // Test buffer size too small
             S3EncryptionClientException exception1 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .setBufferSize(15L)
                     .build()
    @@ -116,8 +104,6 @@ public void testBuilderWithInvalidBufferSizes() throws NoSuchAlgorithmException
             // Test buffer size too large
             S3EncryptionClientException exception2 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .setBufferSize(68719476705L)
                     .build()
    @@ -136,8 +122,6 @@ public void testBuilderWithBufferSizeAndDelayedAuthFails() throws NoSuchAlgorith
                     //= type=test
                     //# If Delayed Authentication mode is enabled, and the buffer size has been set to a value other than its default, the S3EC MUST throw an exception.
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .setBufferSize(1024)
                     .enableDelayedAuthenticationMode(true)
    @@ -154,8 +138,6 @@ public void testBuilderWithNullSecureRandomFails() throws NoSuchAlgorithmExcepti
     
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .secureRandom(null)
                     .build()
    @@ -172,22 +154,22 @@ public void testBuilderWithInvalidCommitmentPolicyAlgorithmCombination() throws
             // Test REQUIRE_ENCRYPT with non-committing algorithm
             S3EncryptionClientException exception1 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                    .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    -                    .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                    .aesKey(aesKey)
    -                    .build()
    +                .aesKey(aesKey)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build()
             );
    -        assertTrue(exception1.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception1.getMessage().contains("commitment policy requires encryption with a committing algorithm"));
     
             // Test FORBID_ENCRYPT with committing algorithm
             S3EncryptionClientException exception2 = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                    .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                    .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    -                    .aesKey(aesKey)
    -                    .build()
    +                .aesKey(aesKey)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .build()
             );
    -        assertTrue(exception2.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception2.getMessage().contains("commitment policy forbids encryption with committing algorithm"));
         }
     
         @Test
    @@ -198,12 +180,11 @@ public void testBuilderWithLegacyAlgorithmFails() throws NoSuchAlgorithmExceptio
     
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                    .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                    .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF)
    -                    .aesKey(aesKey)
    -                    .build()
    +                .aesKey(aesKey)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF)
    +                .build()
             );
    -        assertTrue(exception.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception.getMessage().contains("Encryption algorithm provided is LEGACY"));
         }
     
         @Test
    @@ -213,16 +194,12 @@ public void testBuilderWithWrappedS3EncryptionClientFails() throws NoSuchAlgorit
             SecretKey aesKey = keyGen.generateKey();
     
             S3EncryptionClient wrappedEncryptionClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .build();
     
             // Should not be able to wrap an S3EncryptionClient
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .wrappedClient(wrappedEncryptionClient)
                     .build()
    @@ -239,16 +216,12 @@ public void testBuilderWithWrappedS3AsyncEncryptionClientFails() throws NoSuchAl
             SecretKey aesKey = keyGen.generateKey();
     
             S3AsyncEncryptionClient wrappedAsyncEncryptionClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .build();
     
             // Should not be able to wrap an S3AsyncEncryptionClient
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(aesKey)
                     .wrappedAsyncClient(wrappedAsyncEncryptionClient)
                     .build()
    @@ -267,8 +240,6 @@ public void testBuilderWithInvalidAesKey() throws NoSuchAlgorithmException {
     
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(desKey)
                     .build()
             );
    @@ -285,8 +256,6 @@ public void testBuilderWithInvalidRsaKey() throws NoSuchAlgorithmException {
     
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(ecKey)
                     .build()
             );
    @@ -298,8 +267,6 @@ public void testBuilderWithInvalidRsaKey() throws NoSuchAlgorithmException {
         public void testBuilderWithEmptyKmsKeyId() {
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId("")
                     .build()
             );
    @@ -310,8 +277,6 @@ public void testBuilderWithEmptyKmsKeyId() {
         public void testBuilderWithNullKmsKeyId() {
             S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () ->
                 S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(null)
                     .build()
             );
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientCommitmentPolicyTest.java+175 30 modified
    @@ -68,15 +68,15 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .aesKey(AES_KEY)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build());
    -        assertTrue(exception.getMessage().contains("Both encryption algorithm and commitment policy must be configured."));
    +        assertTrue(exception.getMessage().contains("The commitment policy requires encryption with a committing algorithm suite, but the specified encryption algorithm does not support key commitment."));
             //= specification/s3-encryption/client.md#encryption-algorithm
             //= type=test
             //# The S3EC MUST support configuration of the encryption algorithm (or algorithm suite) during its initialization.
             S3EncryptionClientException exception1 = assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
                     .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .build());
    -        assertTrue(exception1.getMessage().contains("Both encryption algorithm and commitment policy must be configured."));
    +        assertTrue(exception1.getMessage().contains("The commitment policy forbids encryption with committing algorithm suites, but the specified encryption algorithm supports key commitment."));
     
             //= specification/s3-encryption/client.md#encryption-algorithm
             //= type=test
    @@ -89,7 +89,7 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF)
                     .build());
    -        assertTrue(exception2.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception2.getMessage().contains("Encryption algorithm provided is LEGACY! Please specify a fully-supported encryption algorithm."));
     
             // Invalid Configurations
     
    @@ -104,13 +104,13 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .build());
    -        assertTrue(exception3.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception3.getMessage().contains("Encryption algorithm provided is LEGACY! Please specify a fully-supported encryption algorithm."));
             S3EncryptionClientException exception4 = assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
                     .aesKey(AES_KEY)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY)
                     .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
                     .build());
    -        assertTrue(exception4.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception4.getMessage().contains("Encryption algorithm provided is LEGACY! Please specify a fully-supported encryption algorithm."));
     
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
    @@ -120,7 +120,7 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .build());
    -        assertTrue(exception5.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception5.getMessage().contains("The commitment policy forbids encryption with committing algorithm suites, but the specified encryption algorithm supports key commitment."));
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
             //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment.
    @@ -129,7 +129,7 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
                     .build());
    -        assertTrue(exception6.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception6.getMessage().contains("The commitment policy requires encryption with a committing algorithm suite, but the specified encryption algorithm does not support key commitment."));
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
             //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment.
    @@ -138,67 +138,193 @@ public void testCommitmentPolicyAndEncryptionAlgorithm() {
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
                     .build());
    -        assertTrue(exception7.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        assertTrue(exception7.getMessage().contains("The commitment policy requires encryption with a committing algorithm suite, but the specified encryption algorithm does not support key commitment."));
     
         }
     
    +
         @Test
         public void testCommitmentPolicyForbidEncryptAllowDecrypt() {
             final String objectKey = appendTestSuffix("commitment-policy-forbid-encrypt-allow-decrypt");
     
    +        // Create clients with all three commitment policies
    +        S3Client forbidClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        S3Client requireAllowClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .build();
    +
    +        S3Client requireRequireClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .build();
    +
    +        // Test FORBID client encryption and decryption by all clients
    +        final String input = "CommitmentPolicyForbidEncryptAllowDecrypt";
    +        forbidClient.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        Map<String, String> metadata = forbidClient.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(objectKey).build()).metadata();
             //= 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));
    +        assertFalse(ContentMetadataDecodingStrategy.isV3InObjectMetadata(metadata));
    +        assertEquals(metadata.get(MetadataKeyConstants.CONTENT_CIPHER),
    +                AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName());
    +
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
             //# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    +        // FORBID client should be able to decrypt its own encryption
    +        ResponseBytes<GetObjectResponse> forbidResponse = forbidClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, forbidResponse.asUtf8String());
    +
    +        //= specification/s3-encryption/key-commitment.md#commitment-policy
    +        //= type=test
    +        //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    +        // REQUIRE_ALLOW client should be able to decrypt FORBID encryption (allows legacy)
    +        ResponseBytes<GetObjectResponse> requireAllowResponse = requireAllowClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, requireAllowResponse.asUtf8String());
    +
    +        //= specification/s3-encryption/key-commitment.md#commitment-policy
    +        //= type=test
    +        //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST NOT allow decryption using algorithm suites which do not support key commitment.
    +        // REQUIRE_REQUIRE client should NOT be able to decrypt FORBID encryption (requires commitment)
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> requireRequireClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)));
    +        assertTrue(exception.getMessage().contains("Commitment policy violation, decryption requires a committing algorithm suite, but the object was encrypted with a non-committing algorithm."));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, forbidClient);
    +        forbidClient.close();
    +        requireAllowClient.close();
    +        requireRequireClient.close();
    +    }
    +
    +    @Test
    +    public void testCommitmentPolicyRequireEncryptAllowDecrypt() {
    +        final String objectKey = appendTestSuffix("commitment-policy-require-encrypt-allow-decrypt");
    +
    +        // Create clients with all three commitment policies
             S3Client forbidClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        S3Client requireAllowClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .build();
    +
    +        S3Client requireRequireClient = S3EncryptionClient.builderV4()
                     .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
                     .build();
     
    +        // Test REQUIRE_ALLOW client encryption and decryption by all clients
    +        final String input = "CommitmentPolicyRequireEncryptAllowDecrypt";
    +        requireAllowClient.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        Map<String, String> metadata = requireAllowClient.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(objectKey).build()).metadata();
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= 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));
    +        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
             //= type=test
    -        //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    -        S3EncryptionClientException allowDecryptException = assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .aesKey(AES_KEY)
    -                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    -                .build());
    -        assertTrue(allowDecryptException.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +        //# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    +        // FORBID client should be able to decrypt its own encryption
    +        ResponseBytes<GetObjectResponse> forbidResponse = forbidClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, forbidResponse.asUtf8String());
     
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
    -        //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment.
    +        //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    +        // REQUIRE_ALLOW client should be able to decrypt FORBID encryption (allows legacy)
    +        ResponseBytes<GetObjectResponse> requireAllowResponse = requireAllowClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, requireAllowResponse.asUtf8String());
    +
             //= specification/s3-encryption/key-commitment.md#commitment-policy
             //= type=test
             //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST NOT allow decryption using algorithm suites which do not support key commitment.
    -        S3EncryptionClientException requireDecryptException = assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    +        // REQUIRE_REQUIRE client should NOT be able to decrypt FORBID encryption (requires commitment)
    +        ResponseBytes<GetObjectResponse> requireRequireResponse = requireRequireClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, requireRequireResponse.asUtf8String());
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, requireAllowClient);
    +        forbidClient.close();
    +        requireAllowClient.close();
    +        requireRequireClient.close();
    +    }
    +
    +    @Test
    +    public void testCommitmentPolicyRequireEncryptRequireDecrypt() {
    +        final String objectKey = appendTestSuffix("commitment-policy-require-encrypt-require-decrypt");
    +
    +        // Create clients with all three commitment policies
    +        S3Client forbidClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        S3Client requireAllowClient = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .build();
    +
    +        S3Client requireRequireClient = S3EncryptionClient.builderV4()
                     .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    -                .build());
    -        assertTrue(requireDecryptException.getMessage().contains("This client can ONLY be built with these Settings: Commitment Policy: FORBID_ENCRYPT_ALLOW_DECRYPT; Encryption Algorithm: ALG_AES_256_GCM_IV12_TAG16_NO_KDF."));
    +                .build();
     
    -        // Test FORBID client encryption
    -        final String input = "CommitmentPolicyForbidEncryptAllowDecrypt";
    -        forbidClient.putObject(PutObjectRequest.builder()
    +        // Test REQUIRE_REQUIRE client encryption and decryption by all clients
    +        final String input = "CommitmentPolicyRequireEncryptRequireDecrypt";
    +        requireRequireClient.putObject(PutObjectRequest.builder()
                     .bucket(BUCKET)
                     .key(objectKey)
                     .build(), RequestBody.fromString(input));
     
    -        Map<String, String> metadata = forbidClient.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(objectKey).build()).metadata();
    +        Map<String, String> metadata = requireAllowClient.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(objectKey).build()).metadata();
             //= 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));
    -        assertFalse(ContentMetadataDecodingStrategy.isV3InObjectMetadata(metadata));
    -        assertEquals(metadata.get(MetadataKeyConstants.CONTENT_CIPHER),
    -                AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName());
    +        //# 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));
    +        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
             //= type=test
    @@ -209,9 +335,28 @@ public void testCommitmentPolicyForbidEncryptAllowDecrypt() {
                     .key(objectKey));
             assertEquals(input, forbidResponse.asUtf8String());
     
    +        //= specification/s3-encryption/key-commitment.md#commitment-policy
    +        //= type=test
    +        //# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
    +        // REQUIRE_ALLOW client should be able to decrypt FORBID encryption (allows legacy)
    +        ResponseBytes<GetObjectResponse> requireAllowResponse = requireAllowClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, requireAllowResponse.asUtf8String());
    +
    +        //= specification/s3-encryption/key-commitment.md#commitment-policy
    +        //= type=test
    +        //# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST NOT allow decryption using algorithm suites which do not support key commitment.
    +        // REQUIRE_REQUIRE client should NOT be able to decrypt FORBID encryption (requires commitment)
    +        ResponseBytes<GetObjectResponse> requireRequireResponse = requireRequireClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        assertEquals(input, requireRequireResponse.asUtf8String());
    +
             // Cleanup
    -        deleteObject(BUCKET, objectKey, forbidClient);
    +        deleteObject(BUCKET, objectKey, requireRequireClient);
             forbidClient.close();
    +        requireAllowClient.close();
    +        requireRequireClient.close();
         }
    -
     }
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientCompatibilityTest.java+769 68 modified
    @@ -2,6 +2,32 @@
     // SPDX-License-Identifier: Apache-2.0
     package software.amazon.encryption.s3;
     
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.junit.jupiter.api.Assertions.assertNotEquals;
    +import static org.junit.jupiter.api.Assertions.assertThrows;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
    +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject;
    +
    +import java.io.ByteArrayInputStream;
    +import java.io.IOException;
    +import java.nio.charset.StandardCharsets;
    +import java.security.KeyPair;
    +import java.security.KeyPairGenerator;
    +import java.security.NoSuchAlgorithmException;
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import javax.crypto.KeyGenerator;
    +import javax.crypto.SecretKey;
    +
    +import org.junit.jupiter.api.BeforeAll;
    +import org.junit.jupiter.api.Test;
    +
     import com.amazonaws.services.kms.AWSKMS;
     import com.amazonaws.services.kms.AWSKMSClientBuilder;
     import com.amazonaws.services.s3.AmazonS3Encryption;
    @@ -19,8 +45,7 @@
     import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider;
     import com.amazonaws.services.s3.model.SimpleMaterialProvider;
     import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider;
    -import org.junit.jupiter.api.BeforeAll;
    -import org.junit.jupiter.api.Test;
    +
     import software.amazon.awssdk.core.ResponseBytes;
     import software.amazon.awssdk.core.sync.RequestBody;
     import software.amazon.awssdk.services.s3.S3Client;
    @@ -33,26 +58,6 @@
     import software.amazon.encryption.s3.materials.AesKeyring;
     import software.amazon.encryption.s3.materials.MaterialsDescription;
     
    -import javax.crypto.KeyGenerator;
    -import javax.crypto.SecretKey;
    -import java.io.ByteArrayInputStream;
    -import java.io.IOException;
    -import java.nio.charset.StandardCharsets;
    -import java.security.KeyPair;
    -import java.security.KeyPairGenerator;
    -import java.security.NoSuchAlgorithmException;
    -import java.util.HashMap;
    -import java.util.Map;
    -
    -import static org.junit.jupiter.api.Assertions.assertEquals;
    -import static org.junit.jupiter.api.Assertions.assertThrows;
    -import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration;
    -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET;
    -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID;
    -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION;
    -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix;
    -import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject;
    -
     /**
      * This class is an integration test for verifying compatibility of ciphertexts
      * between V1, V2, and V3 clients under various conditions.
    @@ -89,8 +94,6 @@ public void AesCbcV1toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     //= specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
                     //= type=test
    @@ -106,6 +109,8 @@ public void AesCbcV1toV3() {
                     //= type=test
                     //# When enabled, the S3EC MUST be able to decrypt objects encrypted with all content encryption algorithms (both legacy and fully supported).
                     .enableLegacyUnauthenticatedModes(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -123,6 +128,43 @@ public void AesCbcV1toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesCbcV1toV4() {
    +        final String objectKey = appendTestSuffix("aes-cbc-v1-to-v4");
    +
    +        // V1 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        CryptoConfiguration v1CryptoConfig =
    +                new CryptoConfiguration(CryptoMode.EncryptionOnly);
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1CryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                //= specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
    +                //= type=test
    +                //# The S3EC MUST support the option to enable or disable legacy wrapping algorithms.
    +                .aesKey(AES_KEY)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .enableLegacyUnauthenticatedModes(true)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesCbcV1toV4";
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void AesWrapV1toV3() {
             final String objectKey = appendTestSuffix("aes-wrap-v1-to-v3");
    @@ -139,10 +181,10 @@ public void AesWrapV1toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -160,6 +202,40 @@ public void AesWrapV1toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesWrapV1toV4() {
    +        final String objectKey = appendTestSuffix("aes-wrap-v1-to-v4");
    +
    +        // V1 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        CryptoConfiguration v1CryptoConfig =
    +                new CryptoConfiguration(CryptoMode.AuthenticatedEncryption);
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1CryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV1toV4";
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void AesGcmV2toV3() {
             final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3");
    @@ -173,9 +249,9 @@ public void AesGcmV2toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .aesKey(AES_KEY)
                     .build();
     
             // Asserts
    @@ -194,6 +270,37 @@ public void AesGcmV2toV3() {
     
         }
     
    +    @Test
    +    public void AesGcmV2toV4() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v2-to-v4");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV2toV4";
    +        v2Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +
    +    }
    +
         @Test
         public void AesGcmV2toV3WithInstructionFile() {
             final String objectKey = appendTestSuffix("aes-gcm-v2-to-v3-with-instruction-file");
    @@ -211,12 +318,12 @@ public void AesGcmV2toV3WithInstructionFile() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(S3Client.create())
                             .build())
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -235,6 +342,43 @@ public void AesGcmV2toV3WithInstructionFile() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesGcmV2toV4WithInstructionFile() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v2-to-v4-with-instruction-file");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        CryptoConfigurationV2 cryptoConfig =
    +                new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption)
    +                        .withStorageMode(CryptoStorageMode.InstructionFile);
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withCryptoConfiguration(cryptoConfig)
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .instructionFileConfig(InstructionFileConfig.builder()
    +                        .instructionFileClient(S3Client.create())
    +                        .build())
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV2toV4";
    +        v2Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void AesGcmV3toV1() {
             final String objectKey = appendTestSuffix("aes-gcm-v3-to-v1");
    @@ -251,9 +395,9 @@ public void AesGcmV3toV1() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .aesKey(AES_KEY)
                     .build();
     
             // Asserts
    @@ -270,6 +414,52 @@ public void AesGcmV3toV1() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesGcmV4toV1Fails() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v4-to-v1");
    +
    +        // V1 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        CryptoConfiguration v1CryptoConfig =
    +                new CryptoConfiguration(CryptoMode.AuthenticatedEncryption);
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1CryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV4toV1";
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey), RequestBody.fromString(input));
    +
    +        // V1Client in AuthenticatedEncryption decrypts the data first before authenticating the tag and
    +        // returns BAD plaintext
    +        String output = v1Client.getObjectAsString(BUCKET, objectKey);
    +        assertNotEquals(input, output);
    +
    +        // V1 Client in StrictAuthenticatedEncryption
    +        v1CryptoConfig =
    +                new CryptoConfiguration(CryptoMode.StrictAuthenticatedEncryption);
    +        AmazonS3Encryption v1ClientStrict = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1CryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V1Client in StrictAuthenticatedEncryption SHOULD fail to decrypt ciphertext
    +        assertThrows(SecurityException.class, () -> v1ClientStrict.getObjectAsString(BUCKET, objectKey));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void AesGcmV3toV2() {
             final String objectKey = appendTestSuffix("aes-gcm-v3-to-v2");
    @@ -283,9 +473,9 @@ public void AesGcmV3toV2() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .aesKey(AES_KEY)
                     .build();
     
             // Asserts
    @@ -302,15 +492,44 @@ public void AesGcmV3toV2() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesGcmV4toV2Fails() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v4-to-v2");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(AES_KEY));
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV4toV2";
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey), RequestBody.fromString(input));
    +
    +        assertThrows(SecurityException.class, () -> v2Client.getObjectAsString(BUCKET, objectKey));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void AesGcmV3toV3() {
             final String objectKey = appendTestSuffix("aes-gcm-v3-to-v3");
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .aesKey(AES_KEY)
                     .build();
     
             // Asserts
    @@ -331,6 +550,105 @@ public void AesGcmV3toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void AesGcmV3toV4FailsWithRequireDecrypt() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v3-to-v4");
    +
    +        // V4 - Transition Mode Client
    +        S3Client s3ClientTransition = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV3toV4";
    +        s3ClientTransition.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +        s3ClientTransition.close();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class, () -> s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)));
    +        assertTrue(exception.getMessage().contains("Commitment policy violation, decryption requires a committing algorithm suite"));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void AesGcmV4toV4() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v4-to-v4");
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                //= specification/s3-encryption/client.md#cryptographic-materials
    +                //= type=test
    +                //# The S3EC MAY accept key material directly.
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV4toV4";
    +        v4Client.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        ResponseBytes<GetObjectResponse> objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
    +    @Test
    +    public void AesGcmV4toV3() {
    +        final String objectKey = appendTestSuffix("aes-gcm-v4-to-v3");
    +
    +        // V4 - Transition Mode Client
    +        S3Client s3ClientTransition = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .aesKey(AES_KEY)
    +                .build();
    +
    +        // Asserts
    +        final String input = "AesGcmV4toV3";
    +        s3Client.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +        s3ClientTransition.close();
    +
    +        ResponseBytes<GetObjectResponse> objectResponse = s3ClientTransition.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void RsaV1toV3() {
             final String objectKey = appendTestSuffix("v1-rsa-to-v3");
    @@ -342,11 +660,11 @@ public void RsaV1toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .enableLegacyWrappingAlgorithms(true)
                     .enableLegacyUnauthenticatedModes(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             final String input = "This is some content to encrypt using the v1 client";
    @@ -364,6 +682,35 @@ public void RsaV1toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void RsaV1toV4Fails() {
    +        final String objectKey = appendTestSuffix("v1-rsa-to-v4");
    +
    +        EncryptionMaterialsProvider materialsProvider = new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .enableLegacyUnauthenticatedModes(true)
    +                .build();
    +
    +        final String input = "This is some content to encrypt using the v1 client";
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
    +
         @Test
         public void RsaV1toV3AesFails() {
             final String objectKey = appendTestSuffix("v1-rsa-to-v3-aes-fails");
    @@ -374,8 +721,6 @@ public void RsaV1toV3AesFails() {
                     .build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(true)
                     .enableLegacyUnauthenticatedModes(true)
    @@ -409,10 +754,10 @@ public void RsaEcbV1toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .enableLegacyWrappingAlgorithms(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -430,6 +775,40 @@ public void RsaEcbV1toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void RsaEcbV1toV4Fails() {
    +        final String objectKey = appendTestSuffix("rsa-ecb-v1-to-v4");
    +
    +        // V1 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
    +        CryptoConfiguration v1CryptoConfig =
    +                new CryptoConfiguration(CryptoMode.AuthenticatedEncryption);
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1CryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .build();
    +
    +        // Asserts
    +        final String input = "RsaEcbV1toV4";
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void RsaOaepV2toV3() {
             final String objectKey = appendTestSuffix("rsa-oaep-v2-to-v3");
    @@ -446,9 +825,9 @@ public void RsaOaepV2toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
             // Asserts
    @@ -466,6 +845,39 @@ public void RsaOaepV2toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void RsaOaepV2toV4Fails() {
    +        final String objectKey = appendTestSuffix("rsa-oaep-v2-to-v4");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
    +        CryptoConfigurationV2 cryptoConfig =
    +                new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption);
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withCryptoConfiguration(cryptoConfig)
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
    +                .build();
    +
    +        // Asserts
    +        final String input = "RsaOaepV2toV4";
    +        v2Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void RsaOaepV3toV1() {
             final String objectKey = appendTestSuffix("rsa-oaep-v3-to-v1");
    @@ -482,9 +894,9 @@ public void RsaOaepV3toV1() {
     
             // V4 Client - Transition Mode
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
             // Asserts
    @@ -493,7 +905,39 @@ public void RsaOaepV3toV1() {
                     .bucket(BUCKET)
                     .key(objectKey), RequestBody.fromString(input));
     
    -        String output = v1Client.getObjectAsString(BUCKET, objectKey);
    +        String output = v1Client.getObjectAsString(BUCKET, objectKey);
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void RsaOaepV3toV2() {
    +        final String objectKey = appendTestSuffix("rsa-oaep-v3-to-v2");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider =
    +                new StaticEncryptionMaterialsProvider(new EncryptionMaterials(RSA_KEY_PAIR));
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 - Transition Mode Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .build();
    +
    +        // Asserts
    +        final String input = "RsaOaepV3toV2";
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey), RequestBody.fromString(input));
    +
    +        String output = v2Client.getObjectAsString(BUCKET, objectKey);
             assertEquals(input, output);
     
             // Cleanup
    @@ -502,8 +946,8 @@ public void RsaOaepV3toV1() {
         }
     
         @Test
    -    public void RsaOaepV3toV2() {
    -        final String objectKey = appendTestSuffix("rsa-oaep-v3-to-v2");
    +    public void RsaOaepV4toV2Fails() {
    +        final String objectKey = appendTestSuffix("rsa-oaep-v4-to-v2");
     
             // V2 Client
             EncryptionMaterialsProvider materialsProvider =
    @@ -512,21 +956,18 @@ public void RsaOaepV3toV2() {
                     .withEncryptionMaterialsProvider(materialsProvider)
                     .build();
     
    -        // V4 - Transition Mode Client
    +        // V4 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
             // Asserts
    -        final String input = "RsaOaepV3toV2";
    +        final String input = "RsaOaepV4toV2";
             s3Client.putObject(builder -> builder
                     .bucket(BUCKET)
                     .key(objectKey), RequestBody.fromString(input));
     
    -        String output = v2Client.getObjectAsString(BUCKET, objectKey);
    -        assertEquals(input, output);
    +        assertThrows(SecurityException.class, () -> v2Client.getObjectAsString(BUCKET, objectKey));
     
             // Cleanup
             deleteObject(BUCKET, objectKey, s3Client);
    @@ -539,9 +980,9 @@ public void RsaOaepV3toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .rsaKeyPair(RSA_KEY_PAIR)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
             // Asserts
    @@ -562,6 +1003,36 @@ public void RsaOaepV3toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void RsaOaepV4toV4() {
    +        final String objectKey = appendTestSuffix("rsa-oaep-v4-to-v3");
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                //= specification/s3-encryption/client.md#cryptographic-materials
    +                //= type=test
    +                //# The S3EC MAY accept key material directly.
    +                .rsaKeyPair(RSA_KEY_PAIR)
    +                .build();
    +
    +        // Asserts
    +        final String input = "RsaOaepV4toV3";
    +        s3Client.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        ResponseBytes<GetObjectResponse> objectResponse = s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void KmsCBCV1ToV3() {
             String objectKey = appendTestSuffix("v1-kms-cbc-to-v3");
    @@ -579,11 +1050,11 @@ public void KmsCBCV1ToV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .enableLegacyUnauthenticatedModes(true)
                     .enableLegacyWrappingAlgorithms(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             String input = "This is some content to encrypt using v1 client";
    @@ -601,6 +1072,40 @@ public void KmsCBCV1ToV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void KmsCBCV1ToV4Fails() {
    +        String objectKey = appendTestSuffix("v1-kms-cbc-to-v4");
    +
    +        AWSKMS kmsClient = AWSKMSClientBuilder.standard()
    +                .withRegion(KMS_REGION.toString())
    +                .build();
    +        EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ID);
    +
    +        // v1 Client in default mode
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withEncryptionMaterials(materialsProvider)
    +                .withKmsClient(kmsClient)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableLegacyUnauthenticatedModes(true)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .build();
    +
    +        String input = "This is some content to encrypt using v1 client";
    +
    +        v1Client.putObject(BUCKET, objectKey, input);
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build()));
    +
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void KmsV1toV3() {
             final String objectKey = appendTestSuffix("kms-v1-to-v3");
    @@ -620,10 +1125,10 @@ public void KmsV1toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .enableLegacyWrappingAlgorithms(true)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -641,6 +1146,42 @@ public void KmsV1toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void KmsV1toV4() {
    +        final String objectKey = appendTestSuffix("kms-v1-to-v4");
    +
    +        // V1 Client
    +        EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ID);
    +
    +        CryptoConfiguration v1Config =
    +                new CryptoConfiguration(CryptoMode.AuthenticatedEncryption)
    +                        .withStorageMode(CryptoStorageMode.InstructionFile)
    +                        .withAwsKmsRegion(KMS_REGION);
    +
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1Config)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .build();
    +
    +        // Asserts
    +        final String input = "KmsV1toV4";
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void KmsContextV2toV3() {
             final String objectKey = appendTestSuffix("kms-context-v2-to-v3");
    @@ -656,9 +1197,9 @@ public void KmsContextV2toV3() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Asserts
    @@ -685,6 +1226,46 @@ public void KmsContextV2toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void KmsContextV2toV4Fails() {
    +        final String objectKey = appendTestSuffix("kms-context-v2-to-v4");
    +
    +        // V2 Client
    +        EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ID);
    +
    +        CryptoConfigurationV2 config = new CryptoConfigurationV2(CryptoMode.StrictAuthenticatedEncryption);
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .withCryptoConfiguration(config)
    +                .build();
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        // Asserts
    +        final String input = "KmsContextV2toV4";
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value");
    +        EncryptedPutObjectRequest putObjectRequest = new EncryptedPutObjectRequest(
    +                BUCKET,
    +                objectKey,
    +                new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)),
    +                null
    +        ).withMaterialsDescription(encryptionContext);
    +        v2Client.putObject(putObjectRequest);
    +
    +        assertThrows(S3EncryptionClientException.class, () -> v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         // All Below cases should expect failure, since we're writing with V3 Message Format
         @Test
         public void KmsContextV3toV1() {
    @@ -706,9 +1287,9 @@ public void KmsContextV3toV1() {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Asserts
    @@ -730,6 +1311,65 @@ public void KmsContextV3toV1() {
             s3Client.close();
         }
     
    +
    +    @Test
    +    public void KmsContextV4toV1Fails() {
    +        final String objectKey = appendTestSuffix("kms-context-v4-to-v1");
    +
    +        // V1 Client
    +        KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(KMS_KEY_ID);
    +        kmsMaterials.addDescription("user-metadata-key", "user-metadata-value-v4-to-v1");
    +        EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(kmsMaterials);
    +
    +        CryptoConfiguration v1Config =
    +                new CryptoConfiguration(CryptoMode.AuthenticatedEncryption)
    +                        .withAwsKmsRegion(KMS_REGION);
    +
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1Config)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        // Asserts
    +        final String input = "KmsContextV4toV1";
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v4-to-v1");
    +
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext)), RequestBody.fromString(input));
    +
    +
    +        // V1Client in AuthenticatedEncryption decrypts the data first before authenticating the tag and
    +        // returns BAD plaintext
    +        String output = v1Client.getObjectAsString(BUCKET, objectKey);
    +        assertNotEquals(input, output);
    +
    +        // V1 Client in StrictAuthenticatedEncryption
    +        v1Config =
    +                new CryptoConfiguration(CryptoMode.StrictAuthenticatedEncryption)
    +                        .withAwsKmsRegion(KMS_REGION);
    +
    +        AmazonS3Encryption v1ClientStrict = AmazonS3EncryptionClient.encryptionBuilder()
    +                .withCryptoConfiguration(v1Config)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        // V1Client in StrictAuthenticatedEncryption SHOULD fail to decrypt ciphertext
    +        assertThrows(SecurityException.class, () -> v1ClientStrict.getObjectAsString(BUCKET, objectKey));
    +
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void KmsContextV3toV2() throws IOException {
             final String objectKey = appendTestSuffix("kms-context-v3-to-v2");
    @@ -745,9 +1385,9 @@ public void KmsContextV3toV2() throws IOException {
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Asserts
    @@ -768,15 +1408,51 @@ public void KmsContextV3toV2() throws IOException {
             s3Client.close();
         }
     
    +    @Test
    +    public void KmsContextV4toV2Fails() throws IOException {
    +        final String objectKey = appendTestSuffix("kms-context-v4-to-v2");
    +
    +        // V2 Client
    +        KMSEncryptionMaterials kmsMaterials = new KMSEncryptionMaterials(KMS_KEY_ID);
    +        kmsMaterials.addDescription("user-metadata-key", "user-metadata-value-v4-to-v2");
    +        EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(kmsMaterials);
    +
    +        AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
    +                .withEncryptionMaterialsProvider(materialsProvider)
    +                .build();
    +
    +        // V4 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        // Asserts
    +        final String input = "KmsContextV4toV2";
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v4-to-v2");
    +
    +        s3Client.putObject(builder -> builder
    +                        .bucket(BUCKET)
    +                        .key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)),
    +                RequestBody.fromString(input));
    +
    +        assertThrows(SecurityException.class, () -> v2Client.getObjectAsString(BUCKET, objectKey));
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
    +
         @Test
         public void KmsContextV3toV3() {
             final String objectKey = appendTestSuffix("kms-context-v3-to-v3");
     
             // V4 - Transition Mode Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             // Asserts
    @@ -802,14 +1478,47 @@ public void KmsContextV3toV3() {
             s3Client.close();
         }
     
    +    @Test
    +    public void KmsContextV4toV4() {
    +        final String objectKey = appendTestSuffix("kms-context-v4-to-v3");
    +
    +        // V4 Client
    +        S3Client v4Client = S3EncryptionClient.builderV4()
    +                //= specification/s3-encryption/client.md#cryptographic-materials
    +                //= type=test
    +                //# The S3EC MAY accept key material directly.
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        // Asserts
    +        final String input = "KmsContextV4toV3";
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v4-to-v3");
    +
    +        v4Client.putObject(builder -> builder
    +                        .bucket(BUCKET)
    +                        .key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)),
    +                RequestBody.fromString(input));
    +
    +        ResponseBytes<GetObjectResponse> objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext)));
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @Test
         public void KmsContextV3toV3MismatchFails() {
             final String objectKey = appendTestSuffix("kms-context-v3-to-v3");
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
    @@ -853,8 +1562,6 @@ public void AesCbcV1toV3FailsWhenLegacyModeDisabled() {
                     .build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(false)
                     .enableLegacyUnauthenticatedModes(false)
    @@ -897,8 +1604,6 @@ public void AesCbcV1toV3FailsWhenUnauthencticateModeDisabled() {
     
             // V4 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(true)
                     //= specification/s3-encryption/client.md#enable-legacy-unauthenticated-modes
    @@ -939,8 +1644,6 @@ public void AesCbcV1toV3FailsWhenLegacyKeyringDisabled() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     //= specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
                     //= type=test
    @@ -979,8 +1682,6 @@ public void AesWrapV1toV3FailsWhenLegacyModeDisabled() {
                     .build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableLegacyWrappingAlgorithms(false)
                     .build();
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientCRTTest.java+0 6 modified
    @@ -288,8 +288,6 @@ public void AesGcmV3toV3RangedGet() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedAsyncClient(S3AsyncClient.crtCreate())
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    @@ -443,8 +441,6 @@ public void AesCbcV1toV3RangedGet() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedAsyncClient(S3AsyncClient.crtCreate())
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    @@ -533,8 +529,6 @@ public void AesCbcV1toV3FailsRangeExceededObjectLength() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientInstructionFileTest.java+165 22 modified
    @@ -63,8 +63,6 @@ public void testS3EncryptionClientInstructionFileV1V2Format() {
             final String input = "testS3EncryptionClientInstructionFile";
             S3Client wrappedClient = S3Client.create();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     //= specification/s3-encryption/client.md#instruction-file-configuration
                     //= type=test
                     //# The S3EC MAY support the option to provide Instruction File Configuration during its initialization.
    @@ -147,7 +145,120 @@ public void testS3EncryptionClientInstructionFileV1V2Format() {
         }
     
         @Test
    -    public void testV3TransitionInstructionFileExists() {
    +    public void testS3EncryptionClientInstructionFileV3Format() {
    +        final String objectKey = appendTestSuffix("simple-instruction-file-v3-test");
    +        final String input = "testS3EncryptionClientInstructionFile";
    +        S3Client wrappedClient = S3Client.create();
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                //= specification/s3-encryption/client.md#instruction-file-configuration
    +                //= type=test
    +                //# The S3EC MAY support the option to provide Instruction File Configuration during its initialization.
    +                //= specification/s3-encryption/client.md#instruction-file-configuration
    +                //= type=test
    +                //# If the S3EC in a given language supports Instruction Files, then it MUST accept Instruction File Configuration during its initialization.
    +                //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +                //= type=test
    +                //# Instruction File writes MUST be optionally configured during client creation or on each PutObject request.
    +                .instructionFileConfig(InstructionFileConfig.builder()
    +                        .instructionFileClient(wrappedClient)
    +                        .enableInstructionFilePutObject(true)
    +                        .build())
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        // Get the instruction file separately using a default client
    +        S3Client defaultClient = S3Client.create();
    +
    +        ResponseBytes<GetObjectResponse> directGetResponse = defaultClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build());
    +
    +        Map<String, String> objectMetadata = directGetResponse.response().metadata();
    +        //= specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
    +        //= type=test
    +        //# In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata.
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-c" and its value in the Object Metadata when writing with an Instruction File.
    +        assertTrue(objectMetadata.containsKey(MetadataKeyConstants.CONTENT_CIPHER_V3));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-d" and its value in the Object Metadata when writing with an Instruction File.
    +        assertTrue(objectMetadata.containsKey(MetadataKeyConstants.KEY_COMMITMENT_V3));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-i" and its value in the Object Metadata when writing with an Instruction File.
    +        assertTrue(objectMetadata.containsKey(MetadataKeyConstants.MESSAGE_ID_V3));
    +
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +        //= type=test
    +        //# The S3EC MUST support writing some or all (depending on format) content metadata to an Instruction File.
    +        assertFalse(objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3));
    +        assertFalse(objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTION_CONTEXT_V3));
    +        assertFalse(objectMetadata.containsKey(MetadataKeyConstants.MAT_DESC_V3));
    +        assertFalse(objectMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3));
    +
    +        ResponseBytes<GetObjectResponse> instructionFile = defaultClient.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey + ".instruction")
    +                .build());
    +        // Ensure its metadata identifies it as such
    +        assertTrue(instructionFile.response().metadata().containsKey("x-amz-crypto-instr-file"));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +        //= type=test
    +        //# The serialized JSON string MUST be the only contents of the Instruction File.
    +        String instructionFileContent = instructionFile.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +        //= type=test
    +        //# The content metadata stored in the Instruction File MUST be serialized to a JSON string.
    +        Map<String, JsonNode> instructionFileMetadata = parser.parse(instructionFileContent).asObject();
    +
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-c" and its value in the Instruction File.
    +        assertFalse(instructionFileMetadata.containsKey(MetadataKeyConstants.CONTENT_CIPHER_V3));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-d" and its value in the Instruction File.
    +        assertFalse(instructionFileMetadata.containsKey(MetadataKeyConstants.KEY_COMMITMENT_V3));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST NOT store the mapkey "x-amz-i" and its value in the Instruction File.
    +        assertFalse(instructionFileMetadata.containsKey(MetadataKeyConstants.MESSAGE_ID_V3));
    +
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +        //= type=test
    +        //# The S3EC MUST support writing some or all (depending on format) content metadata to an Instruction File.
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-3" and its value in the Instruction File.
    +        assertTrue(instructionFileMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V3));
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-w" and its value in the Instruction File.
    +        assertTrue(instructionFileMetadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM_V3));
    +        // Ensure decryption succeeds
    +        ResponseBytes<GetObjectResponse> objectResponse = s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build());
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +        defaultClient.close();
    +    }
    +
    +    @Test
    +    public void testV4TransitionInstructionFileExists() {
             final String objectKey = appendTestSuffix("instruction-file-put-object");
             final String input = "SimpleTestOfV3EncryptionClient";
             S3Client wrappedClient = S3Client.create();
    @@ -189,13 +300,11 @@ public void testV3TransitionInstructionFileExists() {
         }
     
         @Test
    -    public void testV3InstructionFileExists() {
    +    public void testV4InstructionFileExists() {
             final String objectKey = appendTestSuffix("instruction-file-put-object");
             final String input = "SimpleTestOfV3EncryptionClient";
             S3Client wrappedClient = S3Client.create();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -231,14 +340,13 @@ public void testV3InstructionFileExists() {
         }
     
         @Test
    -    public void testV3DisabledClientFails() {
    +    public void testV4TransitionDisabledClientFails() {
             final String objectKey = appendTestSuffix("instruction-file-put-object-disabled-fails");
             final String input = "SimpleTestOfV3EncryptionClient";
             S3Client wrappedClient = S3Client.create();
             S3Client s3Client = S3EncryptionClient.builderV4()
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -254,8 +362,49 @@ public void testV3DisabledClientFails() {
     
             // Disabled client should fail
             S3Client s3ClientDisabledInstructionFile = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .wrappedClient(wrappedClient)
    +                .instructionFileConfig(InstructionFileConfig.builder()
    +                        .disableInstructionFile(true)
    +                        .build())
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        try {
    +            s3ClientDisabledInstructionFile.getObjectAsBytes(builder -> builder
    +                    .bucket(BUCKET)
    +                    .key(objectKey)
    +                    .build());
    +            fail("expected exception");
    +        } catch (S3EncryptionClientException exception) {
    +            assertTrue(exception.getMessage().contains("Exception encountered while fetching Instruction File."));
    +        }
    +
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +        s3ClientDisabledInstructionFile.close();
    +    }
    +
    +    @Test
    +    public void testV4DisabledClientFails() {
    +        final String objectKey = appendTestSuffix("instruction-file-put-object-disabled-fails");
    +        final String input = "SimpleTestOfV3EncryptionClient";
    +        S3Client wrappedClient = S3Client.create();
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .instructionFileConfig(InstructionFileConfig.builder()
    +                        .instructionFileClient(wrappedClient)
    +                        .enableInstructionFilePutObject(true)
    +                        .build())
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +
    +        // Put with Instruction File
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        // Disabled client should fail
    +        S3Client s3ClientDisabledInstructionFile = S3EncryptionClient.builderV4()
                     .wrappedClient(wrappedClient)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .disableInstructionFile(true)
    @@ -284,7 +433,7 @@ public void testV3DisabledClientFails() {
          * e.g. deleteObjectWithInstructionFileSuccess, but is included anyway to be thorough
          */
         @Test
    -    public void testV3TransitionInstructionFileDelete() {
    +    public void testV4TransitionInstructionFileDelete() {
             final String objectKey = appendTestSuffix("instruction-file-put-object-delete");
             final String input = "SimpleTestOfV3EncryptionClient";
             S3Client wrappedClient = S3Client.create();
    @@ -337,13 +486,11 @@ public void testV3TransitionInstructionFileDelete() {
         }
     
         @Test
    -    public void testV3InstructionFileDelete() {
    +    public void testV4InstructionFileDelete() {
             final String objectKey = appendTestSuffix("instruction-file-put-object-delete");
             final String input = "SimpleTestOfV3EncryptionClient";
             S3Client wrappedClient = S3Client.create();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -514,7 +661,7 @@ public void testPutWithInstructionFileV4TransitionToV2Rsa() throws NoSuchAlgorit
         }
     
         @Test
    -    public void testV3TransitionMultipartPutWithInstructionFile() throws IOException {
    +    public void testV4TransitionMultipartPutWithInstructionFile() throws IOException {
             final String object_key = appendTestSuffix("test-multipart-put-instruction-file");
     
             final long fileSizeLimit = 1024 * 1024 * 50; //50 MB
    @@ -565,7 +712,7 @@ public void testV3TransitionMultipartPutWithInstructionFile() throws IOException
         }
     
         @Test
    -    public void testV3MultipartPutWithInstructionFile() throws IOException {
    +    public void testV4MultipartPutWithInstructionFile() throws IOException {
             final String object_key = appendTestSuffix("test-multipart-put-instruction-file");
     
             final long fileSizeLimit = 1024 * 1024 * 50; //50 MB
    @@ -575,8 +722,6 @@ public void testV3MultipartPutWithInstructionFile() throws IOException {
     
             S3Client wrappedClient = S3Client.create();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
                             .enableInstructionFilePutObject(true)
    @@ -616,7 +761,7 @@ public void testV3MultipartPutWithInstructionFile() throws IOException {
         }
     
         @Test
    -    public void testV3TransitionLowLevelMultipartPutWithInstructionFile() throws NoSuchAlgorithmException, IOException {
    +    public void testV4TransitionLowLevelMultipartPutWithInstructionFile() throws NoSuchAlgorithmException, IOException {
             final String object_key = appendTestSuffix("test-low-level-multipart-put-instruction-file");
     
             final long fileSizeLimit = 1024 * 1024 * 50;
    @@ -718,7 +863,7 @@ public void testV3TransitionLowLevelMultipartPutWithInstructionFile() throws NoS
         }
     
         @Test
    -    public void testV3LowLevelMultipartPutWithInstructionFile() throws NoSuchAlgorithmException, IOException {
    +    public void testV4LowLevelMultipartPutWithInstructionFile() throws NoSuchAlgorithmException, IOException {
             final String object_key = appendTestSuffix("test-low-level-multipart-put-instruction-file");
     
             final long fileSizeLimit = 1024 * 1024 * 50;
    @@ -734,8 +879,6 @@ public void testV3LowLevelMultipartPutWithInstructionFile() throws NoSuchAlgorit
             S3Client wrappedClient = S3Client.create();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(rsaKey)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .instructionFileClient(wrappedClient)
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientMatDescAndEncContextV3FormatTest.java+465 0 added
    @@ -0,0 +1,465 @@
    +package software.amazon.encryption.s3;
    +
    +import org.junit.jupiter.api.BeforeAll;
    +import org.junit.jupiter.api.Test;
    +import software.amazon.awssdk.core.ResponseBytes;
    +import software.amazon.awssdk.core.sync.RequestBody;
    +import software.amazon.awssdk.protocols.jsoncore.JsonNode;
    +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
    +import software.amazon.awssdk.services.s3.S3Client;
    +import software.amazon.awssdk.services.s3.model.GetObjectResponse;
    +import software.amazon.encryption.s3.internal.InstructionFileConfig;
    +import software.amazon.encryption.s3.materials.AesKeyring;
    +import software.amazon.encryption.s3.materials.MaterialsDescription;
    +import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
    +import software.amazon.encryption.s3.materials.RsaKeyring;
    +
    +import javax.crypto.KeyGenerator;
    +import javax.crypto.SecretKey;
    +import java.security.KeyPair;
    +import java.security.KeyPairGenerator;
    +import java.security.NoSuchAlgorithmException;
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.junit.jupiter.api.Assertions.assertNull;
    +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject;
    +
    +public class S3EncryptionClientMatDescAndEncContextV3FormatTest {
    +
    +    private static SecretKey AES_KEY;
    +    private static KeyPair RSA_KEY_PAIR;
    +
    +    @BeforeAll
    +    public static void setUp() throws NoSuchAlgorithmException {
    +        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    +        keyGen.init(256);
    +        AES_KEY = keyGen.generateKey();
    +
    +        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    +        keyPairGen.initialize(2048);
    +        RSA_KEY_PAIR = keyPairGen.generateKeyPair();
    +    }
    +
    +    @Test
    +    public void testKmsEncryptionContextInObjectMetadata() {
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .build();
    +        final String input = "Testing Encryption Context in Object Metadata!";
    +        final String objectKey = appendTestSuffix(
    +                "test-kms-encryption-context-in-object-metadata"
    +        );
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("version", "1.0");
    +        encryptionContext.put("admin", "yes");
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder -> builder.bucket(BUCKET).key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode matDescNode = parser.parse(
    +                responseBytes.response().metadata().get("x-amz-t")
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testAesMaterialsDescriptionInObjectMetadata() {
    +        AesKeyring aesKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "1.0").build()
    +                )
    +                .build();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(aesKeyring)
    +                .build();
    +        final String input = "Testing Materials Description in Object Metadata!";
    +        final String objectKey = appendTestSuffix(
    +                "test-aes-materials-description-in-object-metadata"
    +        );
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        JsonNodeParser parser = JsonNodeParser.create();
    +
    +        JsonNode matDescNode = parser.parse(
    +                responseBytes.response().metadata().get("x-amz-m")
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testRsaMaterialsDescriptionInObjectMetadata() {
    +        PartialRsaKeyPair keyPair = new PartialRsaKeyPair(
    +                RSA_KEY_PAIR.getPrivate(),
    +                RSA_KEY_PAIR.getPublic()
    +        );
    +        RsaKeyring rsaKeyring = RsaKeyring
    +                .builder()
    +                .wrappingKeyPair(keyPair)
    +                .materialsDescription(
    +                        MaterialsDescription
    +                                .builder()
    +                                .put("version", "1.0")
    +                                .put("admin", "yes")
    +                                .build()
    +                )
    +                .build();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(rsaKeyring)
    +                .build();
    +        final String input = "Testing Materials Description in Instruction File!";
    +        final String objectKey = appendTestSuffix(
    +                "test-rsa-materials-description-in-instruction-file"
    +        );
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        JsonNodeParser parser = JsonNodeParser.create();
    +
    +        JsonNode matDescNode = parser.parse(
    +                responseBytes.response().metadata().get("x-amz-m")
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +        assertEquals("yes", matDescNode.asObject().get("admin").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testKmsEncryptionContextInInstructionFile() {
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .enableInstructionFilePutObject(true)
    +                                .instructionFileClient(wrappedClient)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String input = "Testing Encryption Context in Instruction File!";
    +        final String objectKey = appendTestSuffix(
    +                "test-kms-encryption-context-in-instruction-file"
    +        );
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("version", "1.0");
    +        encryptionContext.put("admin", "yes");
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder -> builder.bucket(BUCKET).key(objectKey)
    +                        .overrideConfiguration(withAdditionalConfiguration(encryptionContext)).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        S3Client defaultClient = S3Client.create();
    +
    +        ResponseBytes<GetObjectResponse> directInstGetResponse =
    +                defaultClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = directInstGetResponse.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-t" and its value (when present in the content metadata) in the Instruction File.
    +        JsonNode matDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-t").asString()
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testAesMaterialsDescriptionInInstructionFile() {
    +        AesKeyring aesKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "1.0").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(aesKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .enableInstructionFilePutObject(true)
    +                                .instructionFileClient(wrappedClient)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String input = "Testing Materials Description in Instruction File!";
    +        final String objectKey = appendTestSuffix(
    +                "test-aes-materials-description-in-instruction-file"
    +        );
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        S3Client defaultClient = S3Client.create();
    +
    +        ResponseBytes<GetObjectResponse> directInstGetResponse =
    +                defaultClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = directInstGetResponse.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File.
    +        JsonNode matDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testRsaMaterialsDescriptionInInstructionFile() {
    +        PartialRsaKeyPair keyPair = new PartialRsaKeyPair(
    +                RSA_KEY_PAIR.getPrivate(),
    +                RSA_KEY_PAIR.getPublic()
    +        );
    +
    +        RsaKeyring rsaKeyring = RsaKeyring
    +                .builder()
    +                .wrappingKeyPair(keyPair)
    +                .materialsDescription(
    +                        MaterialsDescription
    +                                .builder()
    +                                .put("version", "1.0")
    +                                .put("admin", "yes")
    +                                .build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(rsaKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .enableInstructionFilePutObject(true)
    +                                .instructionFileClient(wrappedClient)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String input = "Testing Materials Description in Instruction File!";
    +        final String objectKey = appendTestSuffix(
    +                "test-rsa-materials-description-in-instruction-file"
    +        );
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        S3Client defaultClient = S3Client.create();
    +
    +        ResponseBytes<GetObjectResponse> directInstGetResponse =
    +                defaultClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = directInstGetResponse.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
    +        //= type=test
    +        //# - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File.
    +        JsonNode matDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +        assertEquals("yes", matDescNode.asObject().get("admin").asString());
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
    +    @Test
    +    public void testAesKeyringMatDescOverridesPutObjectEncryptionContext() {
    +        AesKeyring aesKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "1.0").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(aesKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .enableInstructionFilePutObject(true)
    +                                .instructionFileClient(wrappedClient)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String input =
    +                "Testing Materials Description in Instruction File and not Encryption Context!";
    +        final String objectKey = appendTestSuffix(
    +                "test-aes-materials-description-in-instruction-file-and-not-encryption-context"
    +        );
    +        final Map<String, String> encryptionContext = new HashMap<String, String>();
    +        encryptionContext.put("admin", "yes");
    +
    +        client.putObject(
    +                builder ->
    +                        builder
    +                                .bucket(BUCKET)
    +                                .key(objectKey)
    +                                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                                .build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        S3Client defaultClient = S3Client.create();
    +
    +        ResponseBytes<GetObjectResponse> directInstGetResponse =
    +                defaultClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = directInstGetResponse.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +
    +        JsonNode matDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +        assertNull(matDescNode.asObject().get("admin"));
    +    }
    +
    +    @Test
    +    public void testRsaKeyringMatDescOverridesPutObjectEncryptionContext() {
    +        PartialRsaKeyPair keyPair = new PartialRsaKeyPair(
    +                RSA_KEY_PAIR.getPrivate(),
    +                RSA_KEY_PAIR.getPublic()
    +        );
    +        RsaKeyring rsaKeyring = RsaKeyring
    +                .builder()
    +                .wrappingKeyPair(keyPair)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "1.0").build()
    +                )
    +                .build();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(rsaKeyring)
    +                .build();
    +        final String input =
    +                "Testing Materials Description in Instruction File and not Encryption Context!";
    +        final String objectKey = appendTestSuffix(
    +                "test-rsa-materials-description-in-instruction-file-and-not-encryption-context"
    +        );
    +        final Map<String, String> encryptionContext = new HashMap<String, String>();
    +        encryptionContext.put("admin", "yes");
    +
    +        client.putObject(
    +                builder ->
    +                        builder
    +                                .bucket(BUCKET)
    +                                .key(objectKey)
    +                                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                                .build(),
    +                RequestBody.fromString(input)
    +        );
    +        ResponseBytes<GetObjectResponse> responseBytes =
    +                client.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +        assertEquals(input, responseBytes.asUtf8String());
    +
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode matDescNode = parser.parse(
    +                responseBytes.response().metadata().get("x-amz-m")
    +        );
    +        assertEquals("1.0", matDescNode.asObject().get("version").asString());
    +        assertNull(matDescNode.asObject().get("admin"));
    +    }
    +}
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientMultipartUploadTest.java+439 6 modified
    @@ -69,8 +69,6 @@ public void multipartPutObjectAsync() throws IOException {
             final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .enableMultipartPutObject(true)
                     .enableDelayedAuthenticationMode(true)
    @@ -103,6 +101,48 @@ public void multipartPutObjectAsync() throws IOException {
             s3Client.close();
         }
     
    +    @Test
    +    public void multipartPutObjectAsyncV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-put-object-async");
    +
    +        final long fileSizeLimit = 1024 * 1024 * 100;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +        final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
    +
    +        S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .enableMultipartPutObject(true)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
    +
    +        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    +
    +        CompletableFuture<PutObjectResponse> futurePut = s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey), AsyncRequestBody.fromInputStream(inputStream, fileSizeLimit, singleThreadExecutor));
    +
    +        futurePut.join();
    +        singleThreadExecutor.shutdown();
    +
    +        // Asserts
    +        CompletableFuture<ResponseInputStream<GetObjectResponse>> getFuture = s3Client.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(S3EncryptionClient.withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey), AsyncResponseTransformer.toBlockingInputStream());
    +        ResponseInputStream<GetObjectResponse> output = getFuture.join();
    +
    +        assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
    +
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +    }
     
         @Test
         public void multipartPutObjectAsyncLargeObjectFails() {
    @@ -113,9 +153,38 @@ public void multipartPutObjectAsyncLargeObjectFails() {
             final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
     
             S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableMultipartPutObject(true)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
    +
    +        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    +
    +        assertThrows(S3EncryptionClientException.class, () -> s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey), AsyncRequestBody.fromInputStream(inputStream, fileSizeLimit, singleThreadExecutor)));
    +
    +        s3Client.close();
    +        singleThreadExecutor.shutdown();
    +    }
    +
    +    @Test
    +    public void multipartPutObjectAsyncV4TransitionLargeObjectFails() {
    +        final String objectKey = appendTestSuffix("multipart-put-object-async-large-object-fails");
    +
    +        // Tight bound on the max GCM limit
    +        final long fileSizeLimit = ((1L << 39) - 256 / 8) + 1;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +
    +        S3AsyncClient s3Client = S3AsyncEncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .enableMultipartPutObject(true)
                     .enableDelayedAuthenticationMode(true)
                     .cryptoProvider(PROVIDER)
    @@ -144,9 +213,44 @@ public void multipartPutObject() throws IOException {
             final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableMultipartPutObject(true)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
    +
    +        s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey), RequestBody.fromInputStream(inputStream, fileSizeLimit));
    +
    +        // Asserts
    +        ResponseInputStream<GetObjectResponse> output = s3Client.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(S3EncryptionClient.withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey));
    +
    +        assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
    +
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void multipartPutObjectV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-put-object");
    +
    +        final long fileSizeLimit = 1024 * 1024 * 100;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +        final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
    +
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(KMS_KEY_ID)
                     .enableMultipartPutObject(true)
                     .enableDelayedAuthenticationMode(true)
                     .cryptoProvider(PROVIDER)
    @@ -280,6 +384,32 @@ public void multipartPutObjectLargeObjectFails() {
             final long fileSizeLimit = ((1L << 39) - 256 / 8) + 1;
             final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
     
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableMultipartPutObject(true)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
    +
    +        assertThrows(S3EncryptionClientException.class, () -> s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey), RequestBody.fromInputStream(inputStream, fileSizeLimit)));
    +
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void multipartPutObjectLargeObjectFailsV4Transition() {
    +        final String objectKey = appendTestSuffix("multipart-put-object-large-fails");
    +
    +        // Tight bound on the max GCM limit
    +        final long fileSizeLimit = ((1L << 39) - 256 / 8) + 1;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +
             S3Client s3Client = S3EncryptionClient.builderV4()
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    @@ -308,6 +438,121 @@ public void multipartUploadV3OutputStream() throws IOException {
             final int PART_SIZE = 10 * 1024 * 1024;
             final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
     
    +        // V3 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        // Create Multipart upload request to S3
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - CreateMultipartUpload MAY be implemented by the S3EC.
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - If implemented, CreateMultipartUpload MUST initiate a multipart upload.
    +        CreateMultipartUploadResponse initiateResult = s3Client.createMultipartUpload(builder ->
    +                builder.bucket(BUCKET).key(objectKey));
    +
    +        List<CompletedPart> partETags = new ArrayList<>();
    +
    +        int bytesRead, bytesSent = 0;
    +        // 10MB parts
    +        byte[] partData = new byte[PART_SIZE];
    +        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    +        int partsSent = 1;
    +
    +        while ((bytesRead = inputStream.read(partData, 0, partData.length)) != -1) {
    +            outputStream.write(partData, 0, bytesRead);
    +            if (bytesSent < PART_SIZE) {
    +                bytesSent += bytesRead;
    +                continue;
    +            }
    +
    +            UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
    +                    .bucket(BUCKET)
    +                    .key(objectKey)
    +                    .uploadId(initiateResult.uploadId())
    +                    .partNumber(partsSent)
    +                    .build();
    +
    +            final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    +            //= specification/s3-encryption/client.md#optional-api-operations
    +            //= type=test
    +            //# - UploadPart MAY be implemented by the S3EC.
    +            //= specification/s3-encryption/client.md#optional-api-operations
    +            //= type=test
    +            //# - UploadPart MUST encrypt each part.
    +            //= specification/s3-encryption/client.md#optional-api-operations
    +            //= type=test
    +            //# - Each part MUST be encrypted in sequence.
    +            //= specification/s3-encryption/client.md#optional-api-operations
    +            //= type=test
    +            //# - Each part MUST be encrypted using the same cipher instance for each part.
    +            UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
    +                    RequestBody.fromInputStream(partInputStream, partInputStream.available()));
    +            partETags.add(CompletedPart.builder()
    +                    .partNumber(partsSent)
    +                    .eTag(uploadPartResult.eTag())
    +                    .build());
    +            outputStream.reset();
    +            bytesSent = 0;
    +            partsSent++;
    +        }
    +        inputStream.close();
    +
    +        // Last Part
    +        UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .uploadId(initiateResult.uploadId())
    +                .partNumber(partsSent)
    +                .sdkPartType(SdkPartType.LAST)
    +                .build();
    +
    +        final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    +        UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
    +                RequestBody.fromInputStream(partInputStream, partInputStream.available()));
    +        partETags.add(CompletedPart.builder()
    +                .partNumber(partsSent)
    +                .eTag(uploadPartResult.eTag())
    +                .build());
    +
    +        // Complete the multipart upload.
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - CompleteMultipartUpload MAY be implemented by the S3EC.
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - CompleteMultipartUpload MUST complete the multipart upload.
    +        s3Client.completeMultipartUpload(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .uploadId(initiateResult.uploadId())
    +                .multipartUpload(partBuilder -> partBuilder.parts(partETags)));
    +
    +        // Asserts
    +        InputStream resultStream = s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)).asInputStream();
    +
    +        assertTrue(IOUtils.contentEquals(new BoundedInputStream(fileSizeLimit), resultStream));
    +        resultStream.close();
    +
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void multipartUploadV3OutputStreamV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-upload-v3-output-stream");
    +
    +        // Overall "file" is 100MB, split into 10MB parts
    +        final long fileSizeLimit = 1024 * 1024 * 100;
    +        final int PART_SIZE = 10 * 1024 * 1024;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    @@ -425,6 +670,100 @@ public void multipartUploadV3OutputStreamPartSize() throws IOException {
             final int PART_SIZE = 10 * 1024 * 1024;
             final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
     
    +        // V3 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        // Create Multipart upload request to S3
    +        CreateMultipartUploadResponse initiateResult = s3Client.createMultipartUpload(builder ->
    +                builder.bucket(BUCKET).key(objectKey));
    +
    +        List<CompletedPart> partETags = new ArrayList<>();
    +
    +        int bytesRead, bytesSent = 0;
    +        // 10MB parts
    +        byte[] partData = new byte[PART_SIZE];
    +        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    +        int partsSent = 1;
    +
    +        while ((bytesRead = inputStream.read(partData, 0, partData.length)) != -1) {
    +            outputStream.write(partData, 0, bytesRead);
    +            if (bytesSent < PART_SIZE) {
    +                bytesSent += bytesRead;
    +                continue;
    +            }
    +
    +            final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    +            UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
    +                    .bucket(BUCKET)
    +                    .key(objectKey)
    +                    .uploadId(initiateResult.uploadId())
    +                    .partNumber(partsSent)
    +                    .contentLength((long) partInputStream.available())
    +                    .build();
    +
    +            UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
    +                    RequestBody.fromInputStream(partInputStream, partInputStream.available()));
    +            partETags.add(CompletedPart.builder()
    +                    .partNumber(partsSent)
    +                    .eTag(uploadPartResult.eTag())
    +                    .build());
    +            outputStream.reset();
    +            bytesSent = 0;
    +            partsSent++;
    +        }
    +
    +        final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    +
    +        // Last Part
    +        UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .uploadId(initiateResult.uploadId())
    +                .partNumber(partsSent)
    +                .contentLength((long) partInputStream.available())
    +                .sdkPartType(SdkPartType.LAST)
    +                .build();
    +
    +        UploadPartResponse uploadPartResult = s3Client.uploadPart(uploadPartRequest,
    +                RequestBody.fromInputStream(partInputStream, partInputStream.available()));
    +        partETags.add(CompletedPart.builder()
    +                .partNumber(partsSent)
    +                .eTag(uploadPartResult.eTag())
    +                .build());
    +
    +        // Complete the multipart upload.
    +        s3Client.completeMultipartUpload(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .uploadId(initiateResult.uploadId())
    +                .multipartUpload(partBuilder -> partBuilder.parts(partETags)));
    +
    +        // Asserts
    +        ResponseBytes<GetObjectResponse> result = s3Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey));
    +
    +        String inputAsString = IoUtils.toUtf8String(new BoundedInputStream(fileSizeLimit));
    +        String outputAsString = IoUtils.toUtf8String(result.asInputStream());
    +        assertEquals(inputAsString, outputAsString);
    +
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void multipartUploadV3OutputStreamPartSizeV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-upload-v3-output-stream-part-size");
    +
    +        // Overall "file" is 30MB, split into 10MB parts
    +        final long fileSizeLimit = 1024 * 1024 * 30;
    +        final int PART_SIZE = 10 * 1024 * 1024;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    @@ -521,6 +860,56 @@ public void multipartUploadV3OutputStreamPartSizeMismatch() throws IOException {
             final int PART_SIZE = 10 * 1024 * 1024;
             final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
     
    +        // V3 Client
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        // Create Multipart upload request to S3
    +        CreateMultipartUploadResponse initiateResult = s3Client.createMultipartUpload(builder ->
    +                builder.bucket(BUCKET).key(objectKey));
    +
    +        int bytesRead, bytesSent = 0;
    +        // 10MB parts
    +        byte[] partData = new byte[PART_SIZE];
    +        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    +        int partsSent = 1;
    +
    +        while ((bytesRead = inputStream.read(partData, 0, partData.length)) != -1) {
    +            outputStream.write(partData, 0, bytesRead);
    +            if (bytesSent < PART_SIZE) {
    +                bytesSent += bytesRead;
    +                continue;
    +            }
    +
    +            final InputStream partInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    +            UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
    +                    .bucket(BUCKET)
    +                    .key(objectKey)
    +                    .uploadId(initiateResult.uploadId())
    +                    .partNumber(partsSent)
    +                    .contentLength((long) partInputStream.available() + 1) // mismatch
    +                    .build();
    +
    +            assertThrows(S3EncryptionClientException.class, () -> s3Client.uploadPart(uploadPartRequest,
    +                    RequestBody.fromInputStream(partInputStream, partInputStream.available())));
    +        }
    +
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
    +    }
    +
    +    @Test
    +    public void multipartUploadV3OutputStreamPartSizeMismatchV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-upload-v3-output-stream-part-size-mismatch");
    +
    +        // Overall "file" is 30MB, split into 10MB parts
    +        final long fileSizeLimit = 1024 * 1024 * 30;
    +        final int PART_SIZE = 10 * 1024 * 1024;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    @@ -573,8 +962,6 @@ public void multipartPutObjectWithOptions() throws IOException {
             final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
     
             final S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
               .kmsKeyId(KMS_KEY_ID)
               .enableMultipartPutObject(true)
               .enableDelayedAuthenticationMode(true)
    @@ -610,4 +997,50 @@ public void multipartPutObjectWithOptions() throws IOException {
             s3Client.close();
         }
     
    +    @Test
    +    public void multipartPutObjectWithOptionsV4Transition() throws IOException {
    +        final String objectKey = appendTestSuffix("multipart-put-object-with-options");
    +
    +        final long fileSizeLimit = 1024 * 1024 * 10;
    +        final InputStream inputStream = new BoundedInputStream(fileSizeLimit);
    +        final InputStream objectStreamForResult = new BoundedInputStream(fileSizeLimit);
    +
    +        final S3Client s3Client = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .kmsKeyId(KMS_KEY_ID)
    +                .enableMultipartPutObject(true)
    +                .enableDelayedAuthenticationMode(true)
    +                .cryptoProvider(PROVIDER)
    +                .build();
    +
    +        final Map<String, String> encryptionContext = new HashMap<>();
    +        encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3");
    +
    +        final StorageClass storageClass = StorageClass.INTELLIGENT_TIERING;
    +
    +        // PutObject
    +        final PutObjectResponse putObjectResponse = s3Client.putObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(withAdditionalConfiguration(encryptionContext))
    +                .storageClass(storageClass)
    +                .key(objectKey), RequestBody.fromInputStream(inputStream, fileSizeLimit));
    +
    +        assertEquals(ChecksumType.FULL_OBJECT.toString(), putObjectResponse.checksumTypeAsString());
    +        assertEquals(ServerSideEncryption.AES256.toString(), putObjectResponse.serverSideEncryptionAsString());
    +
    +        // GetObject
    +        final ResponseInputStream<GetObjectResponse> output = s3Client.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .overrideConfiguration(S3EncryptionClient.withAdditionalConfiguration(encryptionContext))
    +                .key(objectKey));
    +
    +        // Asserts
    +        assertTrue(IOUtils.contentEquals(objectStreamForResult, output));
    +        assertEquals(storageClass, output.response().storageClass());
    +
    +        s3Client.deleteObject(builder -> builder.bucket(BUCKET).key(objectKey));
    +        s3Client.close();
    +    }
    +
     }
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientRangedGetCompatibilityTest.java+178 12 modified
    @@ -13,6 +13,7 @@
     import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider;
     import com.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider;
     import org.junit.jupiter.api.BeforeAll;
    +import org.junit.jupiter.api.Test;
     import org.junit.jupiter.params.ParameterizedTest;
     import org.junit.jupiter.params.provider.MethodSource;
     import software.amazon.awssdk.core.ResponseBytes;
    @@ -25,6 +26,7 @@
     import software.amazon.awssdk.services.s3.model.PutObjectRequest;
     import software.amazon.awssdk.services.s3.model.S3Exception;
     import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
    +import software.amazon.encryption.s3.utils.S3EncryptionClientTestResources;
     
     import javax.crypto.KeyGenerator;
     import javax.crypto.SecretKey;
    @@ -38,6 +40,7 @@
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET;
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ID;
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_REGION;
    +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.TEST_SERVER_BUCKET;
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix;
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject;
     
    @@ -112,6 +115,92 @@ private static AmazonS3EncryptionClientBuilder addKeyMaterialToV1Client(AmazonS3
             return builder;
         }
     
    +    @ParameterizedTest
    +    @MethodSource("keyMaterialProvider")
    +    public void AsyncAesGcmV4toV4RangedGet(Object keyMaterial) {
    +        final String objectKey = appendTestSuffix("async-aes-gcm-v3-to-v3-ranged-get");
    +
    +        final String input = "0bcdefghijklmnopqrst0BCDEFGHIJKLMNOPQRST" +
    +                "1bcdefghijklmnopqrst1BCDEFGHIJKLMNOPQRST" +
    +                "2bcdefghijklmnopqrst2BCDEFGHIJKLMNOPQRST" +
    +                "3bcdefghijklmnopqrst3BCDEFGHIJKLMNOPQRST" +
    +                "4bcdefghijklmnopqrst4BCDEFGHIJKLMNOPQRST";
    +
    +        // Async Client
    +        S3AsyncEncryptionClient.Builder clientBuilder = S3AsyncEncryptionClient.builderV4()
    +                .enableLegacyUnauthenticatedModes(true);
    +        addKeyMaterialTos3Client(clientBuilder, keyMaterial);
    +        S3AsyncClient asyncClient = clientBuilder.build();
    +
    +        asyncClient.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), AsyncRequestBody.fromString(input)).join();
    +
    +        // Valid Range
    +        ResponseBytes<GetObjectResponse> objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                //= specification/s3-encryption/decryption.md#ranged-gets
    +                //= type=test
    +                //# The S3EC MAY support the "range" parameter on GetObject which specifies a subset of bytes to download and decrypt.
    +                .range("bytes=10-20")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(10, 21), output);
    +
    +        // Valid start index within input and end index out of range, returns object from start index to End of Stream
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=190-300")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(190), output);
    +
    +        // Valid start index within input and without specifying end index of range, returns object from start index to End of Stream
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=40-")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(40), output);
    +
    +        // Invalid range with only specifying the end index, returns entire object
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=-40")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range start index range greater than ending index, returns entire object
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=100-50")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range format, returns entire object
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("10-20")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range starting index and ending index greater than object length but within Cipher Block size, returns empty object
    +        objectResponse = asyncClient.getObject(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=216-217")
    +                .key(objectKey), AsyncResponseTransformer.toBytes()).join();
    +        output = objectResponse.asUtf8String();
    +        assertEquals("", output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, asyncClient);
    +        asyncClient.close();
    +    }
    +
         @ParameterizedTest
         @MethodSource("keyMaterialProvider")
         public void AsyncAesGcmV3toV3RangedGet(Object keyMaterial) {
    @@ -211,9 +300,7 @@ public void AsyncFailsOnRangeWhenLegacyModeDisabled(Object keyMaterial) {
                     "4bcdefghijklmnopqrst4BCDEFGHIJKLMNOPQRST";
     
             // V3 Client
    -        S3AsyncEncryptionClient.Builder clientBuilder = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF);
    +        S3AsyncEncryptionClient.Builder clientBuilder = S3AsyncEncryptionClient.builderV4();
             addKeyMaterialTos3Client(clientBuilder, keyMaterial);
             S3AsyncClient asyncClient = clientBuilder.build();
     
    @@ -324,9 +411,7 @@ public void failsOnRangeWhenLegacyModeDisabled(Object keyMaterial) {
                     "4bcdefghijklmnopqrst4BCDEFGHIJKLMNOPQRST";
     
             // V3 Client
    -        S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF);
    +        S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builderV4();
             addKeyMaterialTos3Client(clientBuilder, keyMaterial);
             S3Client s3Client = clientBuilder.build();
     
    @@ -346,6 +431,93 @@ public void failsOnRangeWhenLegacyModeDisabled(Object keyMaterial) {
             s3Client.close();
         }
     
    +    @ParameterizedTest
    +    @MethodSource("keyMaterialProvider")
    +    public void AesGcmV4toV4RangedGet(Object keyMaterial) {
    +        final String objectKey = appendTestSuffix("aes-gcm-v3-to-v3-ranged-get");
    +
    +        final String input = "0bcdefghijklmnopqrst0BCDEFGHIJKLMNOPQRST" +
    +                "1bcdefghijklmnopqrst1BCDEFGHIJKLMNOPQRST" +
    +                "2bcdefghijklmnopqrst2BCDEFGHIJKLMNOPQRST" +
    +                "3bcdefghijklmnopqrst3BCDEFGHIJKLMNOPQRST" +
    +                "4bcdefghijklmnopqrst4BCDEFGHIJKLMNOPQRST";
    +
    +        // V4 Client
    +        S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .enableLegacyUnauthenticatedModes(true);
    +        addKeyMaterialTos3Client(clientBuilder, keyMaterial);
    +        S3Client v4Client = clientBuilder.build();
    +
    +        v4Client.putObject(PutObjectRequest.builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build(), RequestBody.fromString(input));
    +
    +        // Valid Range
    +        ResponseBytes<GetObjectResponse> objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                //= specification/s3-encryption/decryption.md#ranged-gets
    +                //= type=test
    +                //# The S3EC MAY support the "range" parameter on GetObject which specifies a subset of bytes to download and decrypt.
    +                .range("bytes=10-20")
    +                .key(objectKey));
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(10, 21), output);
    +
    +        // Valid start index within input and end index out of range, returns object from start index to End of Stream
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=190-300")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(190), output);
    +
    +        // Valid start index within input and without specifying end index of range, returns object from start index to End of Stream
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=40-")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input.substring(40), output);
    +
    +        // Invalid range with only specifying the end index, returns entire object
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=-40")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range start index range greater than ending index, returns entire object
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=100-50")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range format, returns entire object
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("10-20")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Invalid range starting index and ending index greater than object length but within Cipher Block size, returns empty object
    +        objectResponse = v4Client.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .range("bytes=216-217")
    +                .key(objectKey));
    +        output = objectResponse.asUtf8String();
    +        assertEquals("", output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, v4Client);
    +        v4Client.close();
    +    }
    +
         @ParameterizedTest
         @MethodSource("keyMaterialProvider")
         public void AesGcmV3toV3RangedGet(Object keyMaterial) {
    @@ -447,8 +619,6 @@ public void AesGcmV3toV3FailsRangeExceededObjectLength(Object keyMaterial) {
     
             // V3 Client
             S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .enableLegacyUnauthenticatedModes(true);
             addKeyMaterialTos3Client(clientBuilder, keyMaterial);
             S3Client s3Client = clientBuilder.build();
    @@ -482,8 +652,6 @@ public void AsyncAesGcmV3toV3FailsRangeExceededObjectLength(Object keyMaterial)
     
             // Async Client
             S3AsyncEncryptionClient.Builder clientBuilder = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .enableLegacyUnauthenticatedModes(true);
             addKeyMaterialTos3Client(clientBuilder, keyMaterial);
             S3AsyncClient asyncClient = clientBuilder.build();
    @@ -632,8 +800,6 @@ public void AesCbcV1toV3FailsRangeExceededObjectLength(Object keyMaterial) {
     
             // V3 Client
             S3EncryptionClient.Builder clientBuilder = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .enableLegacyWrappingAlgorithms(true)
                     .enableLegacyUnauthenticatedModes(true);
             addKeyMaterialTos3Client(clientBuilder, keyMaterial);
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileTest.java+575 37 modified
    @@ -54,9 +54,6 @@
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix;
     import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject;
     
    -//= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    -//= type=test
    -//# The S3EC MAY support re-encryption/key rotation via Instruction Files.
     public class S3EncryptionClientReEncryptInstructionFileTest {
     
         private static SecretKey AES_KEY;
    @@ -77,6 +74,240 @@ public static void setUp() throws NoSuchAlgorithmException {
             RSA_KEY_PAIR_TWO = keyPairGen.generateKeyPair();
         }
     
    +    @Test
    +    public void testAesReEncryptInstructionFileFailsWithV2NonCommittingAlgorithmMismatch() {
    +        final String objectKey = appendTestSuffix(
    +                "v2-algorithm-mismatch-test"
    +        );
    +        final String input = "Testing re-encryption failure with V2 non-committing algorithm mismatch";
    +
    +        // Create object with V2 encryption client (non-committing algorithm)
    +        AesKeyring oldKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "v2").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient v2Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        v2Client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +
    +        // Try to re-encrypt with V3 client (committing algorithm) - should fail
    +        AesKeyring newKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY_TWO)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "v3").build()
    +                )
    +                .build();
    +
    +        S3EncryptionClient v3Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest
    +                .builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .newKeyring(newKeyring)
    +                .build();
    +
    +        try {
    +            v3Client.reEncryptInstructionFile(reEncryptRequest);
    +            fail("Expected S3EncryptionClientException for V2 algorithm mismatch");
    +        } catch (S3EncryptionClientException e) {
    +            assertTrue(e.getMessage().contains("Given object is encrypted with non-committing encryption algorithm"));
    +            assertTrue(e.getMessage().contains("ALG_AES_256_GCM_IV12_TAG16_NO_KDF"));
    +            assertTrue(e.getMessage().contains("FORBID_ENCRYPT_ALLOW_DECRYPT"));
    +        }
    +
    +        deleteObject(BUCKET, objectKey, v2Client);
    +    }
    +
    +
    +    @Test
    +    public void testReEncryptInstructionFileFailsWithV3CommittingAlgorithmMismatch() {
    +        final String objectKey = appendTestSuffix(
    +                "v3-algorithm-mismatch-test"
    +        );
    +        final String input = "Testing re-encryption failure with V3 committing algorithm mismatch";
    +
    +        // Create object with V3 encryption client (committing algorithm)
    +        AesKeyring oldKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "v3").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient v3Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        v3Client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +
    +        // Try to re-encrypt with V2 client (non-committing algorithm) - should fail
    +        AesKeyring newKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY_TWO)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "v2").build()
    +                )
    +                .build();
    +
    +        S3EncryptionClient v2Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest
    +                .builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .newKeyring(newKeyring)
    +                .build();
    +
    +        try {
    +            v2Client.reEncryptInstructionFile(reEncryptRequest);
    +            fail("Expected S3EncryptionClientException for V3 algorithm mismatch");
    +        } catch (S3EncryptionClientException e) {
    +            assertTrue(e.getMessage().contains("Given object is encrypted with a committing encryption algorithm"));
    +            assertTrue(e.getMessage().contains("REQUIRE_ENCRYPT_ALLOW_DECRYPT"));
    +            assertTrue(e.getMessage().contains("REQUIRE_ENCRYPT_REQUIRE_DECRYPT"));
    +        }
    +
    +        deleteObject(BUCKET, objectKey, v3Client);
    +    }
    +
    +    @Test
    +    public void testReEncryptInstructionFileFailsWithLegacyAlgorithm() {
    +        final String objectKey = appendTestSuffix(
    +                "legacy-algorithm-mismatch-test"
    +        );
    +        final String input = "Testing re-encryption failure with legacy algorithm";
    +
    +        // Create object with V1 client (legacy algorithm)
    +        EncryptionMaterialsProvider materialsProvider = new StaticEncryptionMaterialsProvider(
    +                new EncryptionMaterials(AES_KEY)
    +                        .addDescription("version", "v1")
    +        );
    +        // Create Keyring with Existing Materials
    +        AesKeyring oldKeyring = AesKeyring.builder()
    +                .wrappingKey(AES_KEY)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .build();
    +
    +        CryptoConfiguration cryptoConfig = new CryptoConfiguration(
    +                CryptoMode.EncryptionOnly
    +        )
    +                .withStorageMode(CryptoStorageMode.InstructionFile);
    +
    +        AmazonS3Encryption v1Client = AmazonS3EncryptionClient
    +                .encryptionBuilder()
    +                .withCryptoConfiguration(cryptoConfig)
    +                .withEncryptionMaterials(materialsProvider)
    +                .build();
    +
    +        v1Client.putObject(BUCKET, objectKey, input);
    +
    +        // Try to re-encrypt with V3 client - should fail with legacy algorithm error
    +        AesKeyring newKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY_TWO)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("version", "v3").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient v3Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)
    +                .enableLegacyUnauthenticatedModes(true)
    +                .enableLegacyWrappingAlgorithms(true)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        ReEncryptInstructionFileRequest reEncryptRequest = ReEncryptInstructionFileRequest
    +                .builder()
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .newKeyring(newKeyring)
    +                .build();
    +
    +        try {
    +            v3Client.reEncryptInstructionFile(reEncryptRequest);
    +            fail("Expected S3EncryptionClientException for legacy algorithm");
    +        } catch (S3EncryptionClientException e) {
    +            System.out.println(e.getMessage());
    +            assertTrue(e.getMessage().contains("Given object is encrypted with legacy content encryption algorithm: AES/CBC/PKCS5Padding."));
    +            assertTrue(e.getMessage().contains("To re-encrypt the data key, configure the client with encryptionAlgorithm ALG_AES_256_GCM_IV12_TAG16_NO_KDF and commitmentPolicy FORBID_ENCRYPT_ALLOW_DECRYPT"));
    +            assertTrue(e.getMessage().contains("and enable legacy unauthenticated modes to use legacy content decryption."));
    +        }
    +
    +        deleteObject(BUCKET, objectKey, v3Client);
    +    }
    +
         @Test
         public void testAesReEncryptInstructionFileFailsWithSameMaterialsDescription() {
             AesKeyring oldKeyring = AesKeyring
    @@ -370,6 +601,334 @@ public void testReEncryptInstructionFileFailsWhenInstructionFilePutNotEnabled()
             }
         }
     
    +    @Test
    +    public void testAesKeyringReEncryptInstructionFile() {
    +        AesKeyring oldKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("rotated", "no").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient s3Client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String objectKey = appendTestSuffix(
    +                "aes-re-encrypt-instruction-file-test"
    +        );
    +        final String input =
    +                "Testing re-encryption of instruction file with AES Keyring";
    +
    +        s3Client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +
    +        ResponseBytes<GetObjectResponse> instructionFile =
    +                wrappedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = instructionFile.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +
    +        String originalEncryptedDataKeyAlgorithm = instructionFileNode
    +                .asObject()
    +                .get("x-amz-w")
    +                .asString();
    +        String originalEncryptedDataKey = instructionFileNode
    +                .asObject()
    +                .get("x-amz-3")
    +                .asString();
    +        JsonNode originalMatDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals(
    +                "no",
    +                originalMatDescNode.asObject().get("rotated").asString()
    +        );
    +
    +        AesKeyring newKeyring = AesKeyring
    +                .builder()
    +                .wrappingKey(AES_KEY_TWO)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("rotated", "yes").build()
    +                )
    +                .build();
    +
    +        ReEncryptInstructionFileRequest reEncryptInstructionFileRequest =
    +                ReEncryptInstructionFileRequest
    +                        .builder()
    +                        .bucket(BUCKET)
    +                        .key(objectKey)
    +                        .newKeyring(newKeyring)
    +                        .build();
    +
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - ReEncryptInstructionFile MAY be implemented by the S3EC.
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM.
    +        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    +        //= type=test
    +        //# The S3EC MAY support re-encryption/key rotation via Instruction Files.
    +        ReEncryptInstructionFileResponse response = s3Client.reEncryptInstructionFile(
    +                reEncryptInstructionFileRequest
    +        );
    +
    +        assertEquals(BUCKET, response.bucket());
    +        assertEquals(objectKey, response.key());
    +        assertEquals("instruction", response.instructionFileSuffix());
    +
    +        S3Client rotatedWrappedClient = S3Client.create();
    +
    +        S3EncryptionClient rotatedClient = S3EncryptionClient
    +                .builderV4()
    +                .keyring(newKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(rotatedWrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        try {
    +            s3Client.getObjectAsBytes(
    +                    GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build()
    +            );
    +            throw new RuntimeException("Expected exception");
    +        } catch (S3EncryptionClientException e) {
    +            assertTrue(e.getMessage().contains("Unable to AES/GCM unwrap"));
    +        }
    +
    +        ResponseBytes<GetObjectResponse> getResponse =
    +                rotatedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +
    +        assertEquals(input, getResponse.asUtf8String());
    +
    +        ResponseBytes<GetObjectResponse> reEncryptedInstructionFile =
    +                rotatedWrappedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String newInstructionFileContent =
    +                reEncryptedInstructionFile.asUtf8String();
    +        JsonNode newInstructionFileNode = parser.parse(newInstructionFileContent);
    +
    +        String postReEncryptionEncryptedDataKeyAlgorithm = newInstructionFileNode
    +                .asObject()
    +                .get("x-amz-w")
    +                .asString();
    +        String postReEncryptionEncryptedDataKey = newInstructionFileNode
    +                .asObject()
    +                .get("x-amz-3")
    +                .asString();
    +        JsonNode postReEncryptionMatDescNode = parser.parse(
    +                newInstructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring.
    +        assertEquals(
    +                "yes",
    +                postReEncryptionMatDescNode.asObject().get("rotated").asString()
    +        );
    +        assertEquals(
    +                originalEncryptedDataKeyAlgorithm,
    +                postReEncryptionEncryptedDataKeyAlgorithm
    +        );
    +        //= specification/s3-encryption/client.md#optional-api-operations
    +        //= type=test
    +        //# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring.
    +        assertNotEquals(originalEncryptedDataKey, postReEncryptionEncryptedDataKey);
    +
    +        deleteObject(BUCKET, objectKey, s3Client);
    +    }
    +
    +    @Test
    +    public void testRsaKeyringReEncryptInstructionFile() {
    +        PublicKey originalPublicKey = RSA_KEY_PAIR.getPublic();
    +        PrivateKey originalPrivateKey = RSA_KEY_PAIR.getPrivate();
    +
    +        PartialRsaKeyPair originalPartialRsaKeyPair = PartialRsaKeyPair
    +                .builder()
    +                .publicKey(originalPublicKey)
    +                .privateKey(originalPrivateKey)
    +                .build();
    +
    +        RsaKeyring oldKeyring = RsaKeyring
    +                .builder()
    +                .wrappingKeyPair(originalPartialRsaKeyPair)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("rotated", "no").build()
    +                )
    +                .build();
    +
    +        S3Client wrappedClient = S3Client.create();
    +        S3EncryptionClient client = S3EncryptionClient
    +                .builderV4()
    +                .keyring(oldKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(wrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        final String objectKey = appendTestSuffix(
    +                "rsa-re-encrypt-instruction-file-test"
    +        );
    +        final String input =
    +                "Testing re-encryption of instruction file with RSA Keyring";
    +
    +        client.putObject(
    +                builder -> builder.bucket(BUCKET).key(objectKey).build(),
    +                RequestBody.fromString(input)
    +        );
    +
    +        ResponseBytes<GetObjectResponse> instructionFile =
    +                wrappedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String instructionFileContent = instructionFile.asUtf8String();
    +        JsonNodeParser parser = JsonNodeParser.create();
    +        JsonNode instructionFileNode = parser.parse(instructionFileContent);
    +
    +        String originalEncryptedDataKeyAlgorithm = instructionFileNode
    +                .asObject()
    +                .get("x-amz-w")
    +                .asString();
    +        String originalEncryptedDataKey = instructionFileNode
    +                .asObject()
    +                .get("x-amz-3")
    +                .asString();
    +        JsonNode originalMatDescNode = parser.parse(
    +                instructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals(
    +                "no",
    +                originalMatDescNode.asObject().get("rotated").asString()
    +        );
    +
    +        PublicKey newPublicKey = RSA_KEY_PAIR_TWO.getPublic();
    +        PrivateKey newPrivateKey = RSA_KEY_PAIR_TWO.getPrivate();
    +
    +        PartialRsaKeyPair newPartialRsaKeyPair = PartialRsaKeyPair
    +                .builder()
    +                .publicKey(newPublicKey)
    +                .privateKey(newPrivateKey)
    +                .build();
    +
    +        RsaKeyring newKeyring = RsaKeyring
    +                .builder()
    +                .wrappingKeyPair(newPartialRsaKeyPair)
    +                .materialsDescription(
    +                        MaterialsDescription.builder().put("rotated", "yes").build()
    +                )
    +                .build();
    +
    +        ReEncryptInstructionFileRequest reEncryptInstructionFileRequest =
    +                ReEncryptInstructionFileRequest
    +                        .builder()
    +                        .bucket(BUCKET)
    +                        .key(objectKey)
    +                        .newKeyring(newKeyring)
    +                        .build();
    +
    +        ReEncryptInstructionFileResponse response = client.reEncryptInstructionFile(
    +                reEncryptInstructionFileRequest
    +        );
    +
    +        assertEquals(BUCKET, response.bucket());
    +        assertEquals(objectKey, response.key());
    +        assertEquals("instruction", response.instructionFileSuffix());
    +        assertFalse(response.enforceRotation());
    +
    +        S3Client rotatedWrappedClient = S3Client.create();
    +
    +        S3EncryptionClient rotatedClient = S3EncryptionClient
    +                .builderV4()
    +                .keyring(newKeyring)
    +                .instructionFileConfig(
    +                        InstructionFileConfig
    +                                .builder()
    +                                .instructionFileClient(rotatedWrappedClient)
    +                                .enableInstructionFilePutObject(true)
    +                                .build()
    +                )
    +                .build();
    +
    +        try {
    +            client.getObjectAsBytes(
    +                    GetObjectRequest.builder().bucket(BUCKET).key(objectKey).build()
    +            );
    +            throw new RuntimeException("Expected exception");
    +        } catch (S3EncryptionClientException e) {
    +            assertTrue(e.getMessage().contains("Unable to RSA-OAEP-SHA1 unwrap"));
    +        }
    +
    +        ResponseBytes<GetObjectResponse> getResponse =
    +                rotatedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey).build()
    +                );
    +
    +        assertEquals(input, getResponse.asUtf8String());
    +
    +        ResponseBytes<GetObjectResponse> reEncryptedInstructionFile =
    +                rotatedWrappedClient.getObjectAsBytes(builder ->
    +                        builder.bucket(BUCKET).key(objectKey + ".instruction").build()
    +                );
    +
    +        String newInstructionFileContent =
    +                reEncryptedInstructionFile.asUtf8String();
    +        JsonNode newInstructionFileNode = parser.parse(newInstructionFileContent);
    +
    +        String postReEncryptionEncryptedDataKeyAlgorithm = newInstructionFileNode
    +                .asObject()
    +                .get("x-amz-w")
    +                .asString();
    +        String postReEncryptionEncryptedDataKey = newInstructionFileNode
    +                .asObject()
    +                .get("x-amz-3")
    +                .asString();
    +        JsonNode postReEncryptionMatDescNode = parser.parse(
    +                newInstructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +
    +        assertEquals(
    +                "yes",
    +                postReEncryptionMatDescNode.asObject().get("rotated").asString()
    +        );
    +        assertEquals(
    +                originalEncryptedDataKeyAlgorithm,
    +                postReEncryptionEncryptedDataKeyAlgorithm
    +        );
    +        assertNotEquals(originalEncryptedDataKey, postReEncryptionEncryptedDataKey);
    +
    +        deleteObject(BUCKET, objectKey, client);
    +    }
    +
         @Test
         public void testV4TransitionAesKeyringReEncryptInstructionFile() {
             AesKeyring oldKeyring = AesKeyring
    @@ -451,15 +1010,6 @@ public void testV4TransitionAesKeyringReEncryptInstructionFile() {
                             .newKeyring(newKeyring)
                             .build();
     
    -        //= specification/s3-encryption/client.md#optional-api-operations
    -        //= type=test
    -        //# - ReEncryptInstructionFile MAY be implemented by the S3EC.
    -        //= specification/s3-encryption/client.md#optional-api-operations
    -        //= type=test
    -        //# - ReEncryptInstructionFile MUST decrypt the instruction file's encrypted data key for the given object using the client's CMM.
    -        //= specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
    -        //= type=test
    -        //# The S3EC MAY support re-encryption/key rotation via Instruction Files.
             ReEncryptInstructionFileResponse response = client.reEncryptInstructionFile(
                     reEncryptInstructionFileRequest
             );
    @@ -526,9 +1076,6 @@ public void testV4TransitionAesKeyringReEncryptInstructionFile() {
                     newInstructionFileNode.asObject().get("x-amz-matdesc").asString()
             );
     
    -        //= specification/s3-encryption/client.md#optional-api-operations
    -        //= type=test
    -        //# - ReEncryptInstructionFile MUST re-encrypt the plaintext data key with a provided keyring.
             assertEquals(
                     "yes",
                     postReEncryptionMatDescNode.asObject().get("rotated").asString()
    @@ -723,7 +1270,7 @@ public void testV4TransitionRsaKeyringReEncryptInstructionFile() {
         }
     
         @Test
    -    public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
    +    public void testRsaKeyringReEncryptInstructionFileWithCustomSuffix() {
             PublicKey clientPublicKey = RSA_KEY_PAIR.getPublic();
             PrivateKey clientPrivateKey = RSA_KEY_PAIR.getPrivate();
     
    @@ -749,8 +1296,6 @@ public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
             S3EncryptionClient client = S3EncryptionClient
                     .builderV4()
                     .keyring(clientKeyring)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .instructionFileConfig(
                             InstructionFileConfig
                                     .builder()
    @@ -807,8 +1352,6 @@ public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
             S3EncryptionClient thirdPartyClient = S3EncryptionClient
                     .builderV4()
                     .keyring(thirdPartyKeyring)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .instructionFileConfig(
                             InstructionFileConfig
                                     .builder()
    @@ -841,23 +1384,22 @@ public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
             JsonNode clientInstructionFileNode = parser.parse(
                     clientInstructionFileContent
             );
    -        String clientIv = clientInstructionFileNode
    -                .asObject()
    -                .get("x-amz-iv")
    -                .asString();
    +
             String clientEncryptedDataKeyAlgorithm = clientInstructionFileNode
                     .asObject()
    -                .get("x-amz-wrap-alg")
    +                .get("x-amz-w")
                     .asString();
             String clientEncryptedDataKey = clientInstructionFileNode
                     .asObject()
    -                .get("x-amz-key-v2")
    +                .get("x-amz-3")
                     .asString();
             JsonNode clientMatDescNode = parser.parse(
    -                clientInstructionFileNode.asObject().get("x-amz-matdesc").asString()
    +                clientInstructionFileNode.asObject().get("x-amz-m").asString()
    +        );
    +        assertEquals(
    +                "yes",
    +                clientMatDescNode.asObject().get("isOwner").asString()
             );
    -
    -        assertEquals("yes", clientMatDescNode.asObject().get("isOwner").asString());
             assertEquals(
                     "admin",
                     clientMatDescNode.asObject().get("access-level").asString()
    @@ -878,20 +1420,17 @@ public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
             JsonNode thirdPartyInstructionFileNode = parser.parse(
                     thirdPartyInstructionFileContent
             );
    -        String thirdPartyIv = thirdPartyInstructionFileNode
    -                .asObject()
    -                .get("x-amz-iv")
    -                .asString();
    +
             String thirdPartyEncryptedDataKeyAlgorithm = thirdPartyInstructionFileNode
                     .asObject()
    -                .get("x-amz-wrap-alg")
    +                .get("x-amz-w")
                     .asString();
             String thirdPartyEncryptedDataKey = thirdPartyInstructionFileNode
                     .asObject()
    -                .get("x-amz-key-v2")
    +                .get("x-amz-3")
                     .asString();
             JsonNode thirdPartyMatDescNode = parser.parse(
    -                thirdPartyInstructionFileNode.asObject().get("x-amz-matdesc").asString()
    +                thirdPartyInstructionFileNode.asObject().get("x-amz-m").asString()
             );
             assertEquals(
                     "no",
    @@ -902,7 +1441,6 @@ public void testV4RsaKeyringReEncryptInstructionFileWithCustomSuffix() {
                     thirdPartyMatDescNode.asObject().get("access-level").asString()
             );
     
    -        assertEquals(clientIv, thirdPartyIv);
             assertEquals(
                     clientEncryptedDataKeyAlgorithm,
                     thirdPartyEncryptedDataKeyAlgorithm
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientReEncryptInstructionFileWithAdditionalDecryptionMaterialsTest.java+19 6 modified
    @@ -11,6 +11,7 @@
     import software.amazon.awssdk.services.s3.S3Client;
     import software.amazon.awssdk.services.s3.model.GetObjectRequest;
     import software.amazon.awssdk.services.s3.model.GetObjectResponse;
    +import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.internal.InstructionFileConfig;
     import software.amazon.encryption.s3.internal.ReEncryptInstructionFileRequest;
     import software.amazon.encryption.s3.internal.ReEncryptInstructionFileResponse;
    @@ -116,7 +117,9 @@ public void testAesKeyringReEncryptInstructionFileWithAdditionalDecryptionMateri
     
             // Create an S3 client for the original encryption
             S3Client wrappedClient = S3Client.create();
    -        S3EncryptionClient originalClient = S3EncryptionClient.builder()
    +        S3EncryptionClient originalClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(originalKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    @@ -162,7 +165,9 @@ public void testAesKeyringReEncryptInstructionFileWithAdditionalDecryptionMateri
                     .build();
     
             // Create a client with the new keyring
    -        S3EncryptionClient newClient = S3EncryptionClient.builder()
    +        S3EncryptionClient newClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(newKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    @@ -297,7 +302,9 @@ public void testRsaKeyringReEncryptInstructionFileWithAdditionalDecryptionMateri
     
             // Create an S3 client for the original encryption
             S3Client wrappedClient = S3Client.create();
    -        S3EncryptionClient originalClient = S3EncryptionClient.builder()
    +        S3EncryptionClient originalClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(originalKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    @@ -343,7 +350,9 @@ public void testRsaKeyringReEncryptInstructionFileWithAdditionalDecryptionMateri
                     .build();
     
             // Create a client with the new keyring
    -        S3EncryptionClient newClient = S3EncryptionClient.builder()
    +        S3EncryptionClient newClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(newKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    @@ -478,7 +487,9 @@ public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixAndAdditionalD
     
             // Create an S3 client for the original encryption
             S3Client wrappedClient = S3Client.create();
    -        S3EncryptionClient originalClient = S3EncryptionClient.builder()
    +        S3EncryptionClient originalClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(originalKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    @@ -525,7 +536,9 @@ public void testRsaKeyringReEncryptInstructionFileWithCustomSuffixAndAdditionalD
                     .build();
     
             // Create a client with the new keyring
    -        S3EncryptionClient newClient = S3EncryptionClient.builder()
    +        S3EncryptionClient newClient = S3EncryptionClient.builderV4()
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(newKeyring)
                     .instructionFileConfig(
                             InstructionFileConfig.builder()
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientRsaKeyPairTest.java+0 9 modified
    @@ -9,7 +9,6 @@
     import software.amazon.awssdk.services.s3.S3Client;
     import software.amazon.awssdk.services.s3.model.GetObjectResponse;
     import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    -import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
     import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
     
     import java.security.KeyPair;
    @@ -39,8 +38,6 @@ public void RsaPublicAndPrivateKeys() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
    @@ -66,14 +63,10 @@ public void RsaPublicAndPrivateKeys() {
         public void RsaPrivateKeyCanOnlyDecrypt() {
             final String objectKey = appendTestSuffix("rsa-private-key-only");
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .build();
     
             S3Client s3ClientReadOnly = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(new PartialRsaKeyPair(RSA_KEY_PAIR.getPrivate(), null))
                     .build();
     
    @@ -103,8 +96,6 @@ public void RsaPrivateKeyCanOnlyDecrypt() {
         public void RsaPublicKeyCanOnlyEncrypt() {
             final String objectKey = appendTestSuffix("rsa-public-key-only");
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(new PartialRsaKeyPair(null, RSA_KEY_PAIR.getPublic()))
                     .build();
     
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientStreamTest.java+1 40 modified
    @@ -46,7 +46,6 @@
     import java.util.concurrent.Executors;
     
     import static org.junit.jupiter.api.Assertions.assertEquals;
    -import static org.junit.jupiter.api.Assertions.assertInstanceOf;
     import static org.junit.jupiter.api.Assertions.assertNotEquals;
     import static org.junit.jupiter.api.Assertions.assertThrows;
     import static org.junit.jupiter.api.Assertions.assertTrue;
    @@ -77,8 +76,6 @@ public void markResetInputStreamV3Encrypt() throws IOException {
             final String objectKey = appendTestSuffix("markResetInputStreamV3Encrypt");
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -112,8 +109,6 @@ public void ordinaryInputStreamV3Encrypt() throws IOException {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -164,8 +159,6 @@ public void ordinaryInputStreamV3UnboundedAsync() {
         @Test
         public void ordinaryInputStreamV3UnboundedMultipartAsync() {
             try (S3AsyncClient s3AsyncEncryptionClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableMultipartPutObject(true)
                     .build()) {
    @@ -186,8 +179,6 @@ public void ordinaryInputStreamV3UnboundedMultipartAsync() {
         public void ordinaryInputStreamV3UnboundedCrt() {
             try (S3AsyncClient s3CrtAsyncClient = S3AsyncClient.crtCreate()) {
                 try (S3AsyncClient s3AsyncEncryptionClient = S3AsyncEncryptionClient.builderV4()
    -                    .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                    .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                         .aesKey(AES_KEY)
                         .enableMultipartPutObject(true)
                         .wrappedClient(s3CrtAsyncClient)
    @@ -212,8 +203,6 @@ public void ordinaryInputStreamV3Decrypt() throws IOException {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -290,28 +279,20 @@ public void ordinaryInputStreamV3DecryptCbc() throws IOException {
         @Test
         public void invalidBufferSize() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
     
                     .setBufferSize(15L)
                     .build());
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .setBufferSize(68719476705L)
                     .build());
     
             assertThrows(S3EncryptionClientException.class, () -> S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .setBufferSize(15L)
                     .build());
             assertThrows(S3EncryptionClientException.class, () -> S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .setBufferSize(68719476705L)
                     .build());
    @@ -320,16 +301,12 @@ public void invalidBufferSize() {
         @Test
         public void failsWhenBothBufferSizeAndDelayedAuthModeEnabled() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .setBufferSize(16)
                     .enableDelayedAuthenticationMode(true)
                     .build());
     
             assertThrows(S3EncryptionClientException.class, () -> S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .setBufferSize(16)
                     .enableDelayedAuthenticationMode(true)
    @@ -345,8 +322,6 @@ public void customSetBufferSizeWithLargeObject() throws IOException {
     
             // V3 Client with custom max buffer size 32 MiB.
             S3Client s3ClientWithBuffer32MiB = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     //= specification/s3-encryption/client.md#set-buffer-size
    @@ -359,8 +334,6 @@ public void customSetBufferSizeWithLargeObject() throws IOException {
             // V3 Client with default buffer size (i.e. 64MiB)
             // When enableDelayedAuthenticationMode is set to true, delayed authentication mode always takes priority over buffered mode.
             S3Client s3ClientWithDelayedAuth = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     .enableDelayedAuthenticationMode(true)
    @@ -405,8 +378,6 @@ public void customSetBufferSizeWithLargeObjectAsyncClient() throws IOException {
     
             // V3 Client with custom max buffer size 32 MiB.
             S3AsyncClient s3ClientWithBuffer32MiB = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     .setBufferSize(32 * 1024 * 1024)
    @@ -415,8 +386,6 @@ public void customSetBufferSizeWithLargeObjectAsyncClient() throws IOException {
             // V3 Client with default buffer size (i.e. 64MiB)
             // When enableDelayedAuthenticationMode is set to true, delayed authentication mode always takes priority over buffered mode.
             S3AsyncClient s3ClientWithDelayedAuth = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     .enableDelayedAuthenticationMode(true)
    @@ -469,8 +438,6 @@ public void delayedAuthModeWithLargeObject() throws IOException {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     //= specification/s3-encryption/client.md#enable-delayed-authentication
                     //= type=test
    @@ -497,8 +464,6 @@ public void delayedAuthModeWithLargeObject() throws IOException {
                     .key(objectKey)));
     
             S3Client s3ClientWithDelayedAuth = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     //= specification/s3-encryption/client.md#enable-delayed-authentication
                     //= type=test
    @@ -529,8 +494,6 @@ public void delayedAuthModeWithLargerThanMaxObjectFails() throws IOException {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .enableDelayedAuthenticationMode(true)
                     .build();
    @@ -557,8 +520,6 @@ public void AesGcmV3toV3StreamWithTamperedTag() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -620,7 +581,7 @@ public void AesGcmV3toV3StreamWithTamperedTag() {
             try {
                 dataStream.read(chunk1, 0, chunkSize);
             } catch (RuntimeException outerEx) {
    -            assertInstanceOf(AEADBadTagException.class, outerEx.getCause());
    +            assertTrue(outerEx.getCause() instanceof AEADBadTagException);
             } catch (IOException unexpected) {
                 // Not expected, but fail the test anyway
                 fail(unexpected);
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java+77 93 modified
    @@ -122,8 +122,6 @@ public void copyObjectTransparently() {
             final String newObjectKey = appendTestSuffix("copy-object-to-here");
     
             S3Client s3EncryptionClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
    @@ -172,8 +170,6 @@ public void deleteObjectWithInstructionFileSuccess() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             final String input = "DeleteObjectWithInstructionFileSuccess";
    @@ -223,8 +219,6 @@ public void deleteObjectsWithInstructionFilesSuccess() {
     
             // V4 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             final String input = "DeleteObjectsWithInstructionFileSuccess";
    @@ -265,8 +259,6 @@ public void deleteObjectsWithInstructionFilesSuccess() {
         public void deleteObjectWithWrongObjectKeySuccess() {
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             assertDoesNotThrow(() -> s3Client.deleteObject(builder -> builder.bucket(BUCKET).key("InvalidKey")));
    @@ -279,8 +271,6 @@ public void deleteObjectWithWrongObjectKeySuccess() {
         public void deleteObjectWithWrongBucketFailure() {
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             try {
    @@ -297,8 +287,6 @@ public void deleteObjectWithWrongBucketFailure() {
         public void deleteObjectsWithWrongBucketFailure() {
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             List<ObjectIdentifier> objects = new ArrayList<>();
    @@ -316,8 +304,6 @@ public void deleteObjectsWithWrongBucketFailure() {
         public void getNonExistentObject() {
             final String objectKey = appendTestSuffix("this-is-not-an-object-key");
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ALIAS)
                     .build();
     
    @@ -346,8 +332,6 @@ public void getNonExistentObject() {
         @Test
         public void s3EncryptionClientWithMultipleKeyringsFails() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .rsaKeyPair(RSA_KEY_PAIR)
                     .build());
    @@ -364,8 +348,6 @@ public void s3EncryptionClientWithCMMAndKeyringFails() {
                             .build())
                     .build();
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoMaterialsManager(defaultCMM)
                     .build());
    @@ -374,16 +356,12 @@ public void s3EncryptionClientWithCMMAndKeyringFails() {
         @Test
         public void s3EncryptionClientWithNoKeyringsFails() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build());
         }
     
         @Test
         public void s3EncryptionClientWithNoLegacyKeyringsFails() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .enableLegacyWrappingAlgorithms(true)
                     .build());
         }
    @@ -394,15 +372,12 @@ public void testDefaultContentMetadataStorage() {
     
             // V4 Client
             S3Client v4Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                //= specification/s3-encryption/encryption.md#content-encryption
    -                //= type=test
    -                //# The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     //= specification/s3-encryption/client.md#cryptographic-materials
                     //= type=test
                     //# The S3EC MAY accept key material directly.
                     .aesKey(AES_KEY)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .build();
     
             // Asserts
    @@ -441,8 +416,6 @@ public void testDefaultContentMetadataStorage() {
         public void KmsWithAliasARN() {
             final String objectKey = appendTestSuffix("kms-with-alias-arn");
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ALIAS)
                     .build();
     
    @@ -461,8 +434,6 @@ public void KmsWithShortKeyId() {
             final String shortId = KMS_KEY_ID.split("/")[1];
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(shortId)
                     .build();
     
    @@ -477,17 +448,13 @@ public void KmsWithShortKeyId() {
         public void KmsAliasARNToKeyId() {
             final String objectKey = appendTestSuffix("kms-alias-arn-to-key-id");
             S3Client aliasClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     //= specification/s3-encryption/client.md#cryptographic-materials
                     //= type=test
                     //# The S3EC MAY accept key material directly.
                     .kmsKeyId(KMS_KEY_ALIAS)
                     .build();
     
             S3Client keyIdClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
    @@ -521,8 +488,6 @@ public void AesKeyringWithInvalidAesKey() throws NoSuchAlgorithmException {
             invalidAesKey = keyGen.generateKey();
     
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(invalidAesKey)
                     .build());
         }
    @@ -535,8 +500,6 @@ public void RsaKeyringWithInvalidRsaKey() throws NoSuchAlgorithmException {
             invalidRsaKey = keyPairGen.generateKeyPair();
     
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .rsaKeyPair(invalidRsaKey)
                     .build());
         }
    @@ -549,8 +512,6 @@ public void s3EncryptionClientWithKeyringFromKmsKeyIdSucceeds() {
             KmsKeyring keyring = KmsKeyring.builder().wrappingKeyId(KMS_KEY_ID).build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     //= specification/s3-encryption/client.md#cryptographic-materials
                     //= type=test
                     //# When a Keyring is provided, the S3EC MUST create an instance of the DefaultCMM using the provided Keyring.
    @@ -575,8 +536,6 @@ public void s3EncryptionClientWithCmmFromKmsKeyIdSucceeds() {
                     .build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .cryptoMaterialsManager(cmm)
                     .build();
     
    @@ -598,8 +557,6 @@ public void s3EncryptionClientWithWrappedS3ClientSucceeds() {
             S3AsyncClient wrappedAsyncClient = S3AsyncClient.create();
     
             S3Client wrappingClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedClient(wrappedClient)
                     .wrappedAsyncClient(wrappedAsyncClient)
                     .kmsKeyId(KMS_KEY_ID)
    @@ -625,14 +582,10 @@ public void s3EncryptionClientWithWrappedS3ClientSucceeds() {
         @Test
         public void s3EncryptionClientWithWrappedS3EncryptionClientFails() {
             S3AsyncClient wrappedAsyncClient = S3AsyncEncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedAsyncClient(wrappedAsyncClient)
                     .kmsKeyId(KMS_KEY_ID)
                     .build());
    @@ -641,8 +594,6 @@ public void s3EncryptionClientWithWrappedS3EncryptionClientFails() {
         @Test
         public void s3EncryptionClientWithNullSecureRandomFails() {
             assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .secureRandom(null)
                     .build());
    @@ -652,8 +603,6 @@ public void s3EncryptionClientWithNullSecureRandomFails() {
         public void s3EncryptionClientWithSecureRandom() {
             SecureRandom secureRandom = new SecureRandom();
             S3EncryptionClient s3EncryptionClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     //= specification/s3-encryption/client.md#randomness
                     //= type=test
    @@ -670,8 +619,6 @@ public void s3EncryptionClientFromKMSKeyDoesNotUseUnprovidedSecureRandom() {
             final String objectKey = appendTestSuffix("no-secure-random-object-kms");
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
    @@ -693,8 +640,6 @@ public void cryptoProviderV3toV3Enabled() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .cryptoProvider(provider)
                     .build();
    @@ -759,8 +704,6 @@ public void contentLengthRequest() {
             final String objectKey = appendTestSuffix("content-length");
     
             S3Client s3EncryptionClient = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ID)
                     .build();
     
    @@ -794,8 +737,6 @@ public void attemptToDecryptPlaintext() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -824,8 +765,6 @@ public void attemptToDecryptPlaintext() {
         public void createMultipartUploadFailure() {
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             try {
    @@ -843,8 +782,6 @@ public void uploadPartFailure() {
             final String objectKey = appendTestSuffix("upload-part-failure");
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -877,8 +814,6 @@ public void uploadPartFailure() {
         public void completeMultipartUploadFailure() {
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
             try {
    @@ -897,8 +832,6 @@ public void abortMultipartUploadFailure() {
     
             // V3 Client
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .aesKey(AES_KEY)
                     .build();
     
    @@ -938,8 +871,6 @@ public void s3EncryptionClientWithCustomCredentials() {
                     .wrappingKeyId(KMS_KEY_ID)
                     .build();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedClient(wrappedClient)
                     .wrappedAsyncClient(wrappedAsyncClient)
                     .keyring(keyring)
    @@ -961,8 +892,6 @@ public void s3EncryptionClientTopLevelAllOptions() {
             // use all top-level options;
             // there isn't a good way to validate every option.
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -998,8 +927,6 @@ public void s3EncryptionClientTopLevelCredentials() {
             AwsCredentialsProvider creds = DefaultCredentialsProvider.create();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -1020,8 +947,6 @@ public void s3EncryptionClientTopLevelCredentialsWrongRegion() {
             AwsCredentialsProvider creds = DefaultCredentialsProvider.create();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of("eu-west-1"))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -1046,8 +971,6 @@ public void s3EncryptionClientTopLevelCredentialsNullCreds() {
             AwsCredentialsProvider creds = new S3EncryptionClientTestResources.NullCredentialsProvider();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -1079,8 +1002,6 @@ public void s3EncryptionClientTopLevelAlternateCredentials() {
             AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(KMS_KEY_ID)
    @@ -1100,8 +1021,6 @@ public void s3EncryptionClientTopLevelAlternateCredentials() {
     
             // using the alternate key succeeds
             S3Client s3ClientAltCreds = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .region(Region.of(KMS_REGION.toString()))
                     .kmsKeyId(ALTERNATE_KMS_KEY)
    @@ -1131,8 +1050,6 @@ public void s3EncryptionClientMixedCredentials() {
                     .build();
     
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .keyring(kmsKeyring)
                     .build();
     
    @@ -1208,8 +1125,6 @@ public void s3EncryptionClientMixedCredentialsInstructionFileFails() {
             // use alternate creds for KMS
             AwsCredentialsProvider creds = new S3EncryptionClientTestResources.AlternateRoleCredentialsProvider();
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .credentialsProvider(creds)
                     .kmsKeyId(ALTERNATE_KMS_KEY)
                     .build();
    @@ -1234,8 +1149,6 @@ public void s3EncryptionClientMixedCredentialsInstructionFileFails() {
     
             // Default creds should fail
             S3Client s3ClientDefault = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(ALTERNATE_KMS_KEY)
                     .build();
             try {
    @@ -1258,8 +1171,6 @@ public void NonUSASCIIMetadataFails() {
             final String objectKey = appendTestSuffix("non-us-ascii-metadata-fails");
             final String input = "This is a test.";
             S3Client s3Client = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .kmsKeyId(KMS_KEY_ALIAS)
                     .build();
     
    @@ -1301,8 +1212,6 @@ public void testInstructionFileConfig() {
     
             S3Client wrappedClient = S3Client.create();
             S3Client s3ClientDisabledInstructionFile = S3EncryptionClient.builderV4()
    -                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    -                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
                     .wrappedClient(wrappedClient)
                     .instructionFileConfig(InstructionFileConfig.builder()
                             .disableInstructionFile(true)
    @@ -1343,6 +1252,81 @@ public void testInstructionFileConfig() {
             s3Client.close();
         }
     
    +    @Test
    +    public void testSimpleKCRoundTrip() {
    +        final String objectKey = appendTestSuffix("simple-kc-roundtrip");
    +        S3Client s3Client = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ALIAS)
    +                //= specification/s3-encryption/encryption.md#content-encryption
    +                //= type=test
    +                //# The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .build();
    +
    +        simpleV3RoundTrip(s3Client, objectKey);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3Client);
    +        s3Client.close();
    +
    +    }
    +
    +    @Test
    +    public void testRejectNonCommitting() {
    +        final String objectKey = appendTestSuffix("kc-reject-non-committing");
    +        S3Client s3ClientPutsNonKC = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ALIAS)
    +                //= specification/s3-encryption/encryption.md#content-encryption
    +                //= type=test
    +                //# The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    +                .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
    +                .build();
    +
    +        final String input = "SimpleTestOfV3EncryptionClient";
    +
    +        s3ClientPutsNonKC.putObject(builder -> builder
    +                        .bucket(BUCKET)
    +                        .key(objectKey)
    +                        .build(),
    +                RequestBody.fromString(input));
    +
    +        S3Client s3ClientRequireDecrypt = S3EncryptionClient.builderV4()
    +                .kmsKeyId(KMS_KEY_ALIAS)
    +                .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY)
    +                .commitmentPolicy(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
    +                .build();
    +
    +        //= specification/s3-encryption/decryption.md#key-commitment
    +        //= type=test
    +        //# The S3EC MUST validate the algorithm suite used for decryption against the key commitment policy before attempting to decrypt the content ciphertext.
    +        //= specification/s3-encryption/decryption.md#key-commitment
    +        //= type=test
    +        //# If the commitment policy requires decryption using a committing algorithm suite, and the algorithm suite associated with the object does not support key commitment, then the S3EC MUST throw an exception.
    +        try {
    +            s3ClientRequireDecrypt.getObjectAsBytes(builder -> builder
    +                    .bucket(BUCKET)
    +                    .key(objectKey)
    +                    .build());
    +            fail("expected exception");
    +        } catch (S3EncryptionClientException exception) {
    +            // expected
    +        }
    +
    +        ResponseBytes<GetObjectResponse> objectResponse = s3ClientPutsNonKC.getObjectAsBytes(builder -> builder
    +                .bucket(BUCKET)
    +                .key(objectKey)
    +                .build());
    +        String output = objectResponse.asUtf8String();
    +        assertEquals(input, output);
    +
    +        // Cleanup
    +        deleteObject(BUCKET, objectKey, s3ClientPutsNonKC);
    +        s3ClientPutsNonKC.close();
    +
    +    }
    +
         /**
          * A simple, reusable round-trip (encryption + decryption) using a given
          * S3Client. Useful for testing client configuration.
    
  • src/test/java/software/amazon/encryption/s3/S3EncryptionClientTestVectorsTest.java+1 1 modified
    @@ -25,9 +25,9 @@ public class S3EncryptionClientTestVectorsTest {
         @Test
         public void decryptUnicodeTestVectors() {
             S3Client s3EncryptionClient = S3EncryptionClient.builderV4()
    +                .kmsKeyId(TESTVECTORS_KMS_KEY)
                     .commitmentPolicy(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
                     .encryptionAlgorithm(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF)
    -                .kmsKeyId(TESTVECTORS_KMS_KEY)
                     .region(Region.of("us-west-2"))
                     .build();
             // Reuse s3EncryptionClient, even though this operation doesn't require encryption
    
  • SUPPORT_POLICY.rst+4 0 modified
    @@ -30,6 +30,10 @@ This table describes the current support status of each major version of the Ama
           - End of Support
           - EOY 2025
         * - 3.x
    +      - Maintenance Mode
    +      -
    +      -
    +    * - 4.x
           - Generally Available
           -
           -
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.