This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 34ca224bade939856d82a45986ba53ab63c2fd78
Author: TungTV <vtt...@linagora.com>
AuthorDate: Tue Nov 19 10:17:14 2024 +0700

    JAMES-4085 [S3 SSEC] SSEC basic implementation
---
 .../blob/objectstorage/aws/S3BlobStoreDAO.java     |  44 ++++++--
 .../blob/objectstorage/aws/S3RequestOption.java    |  38 +++++++
 .../objectstorage/aws/sse/S3SSECConfiguration.java |  46 ++++++++
 .../aws/sse/S3SSECustomerKeyFactory.java           |  61 +++++++++++
 .../aws/sse/S3SSECustomerKeyGenerator.java         |  55 ++++++++++
 .../aws/sse/S3BlobStoreDAOWithSSECTest.java        | 120 +++++++++++++++++++++
 .../blob/objectstorage/aws/sse/S3SSECContract.java |  89 +++++++++++++++
 .../aws/sse/S3SSECustomerKeyGeneratorTest.java     |  61 +++++++++++
 .../aws/sse/SingleCustomerKeyFactoryTest.java      |  65 +++++++++++
 9 files changed, 572 insertions(+), 7 deletions(-)

diff --git 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
index 9b4b993de3..8c34c3a6b6 100644
--- 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
+++ 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
@@ -114,14 +114,23 @@ public class S3BlobStoreDAO implements BlobStoreDAO {
     private final S3AsyncClient client;
     private final S3BlobStoreConfiguration configuration;
     private final BlobId.Factory blobIdFactory;
+    private final S3RequestOption s3RequestOption;
 
     @Inject
     public S3BlobStoreDAO(S3ClientFactory s3ClientFactory,
-                   S3BlobStoreConfiguration configuration,
-                   BlobId.Factory blobIdFactory) {
+                          S3BlobStoreConfiguration configuration,
+                          BlobId.Factory blobIdFactory) {
+       this(s3ClientFactory, configuration, blobIdFactory, 
S3RequestOption.DEFAULT);
+    }
+
+    public S3BlobStoreDAO(S3ClientFactory s3ClientFactory,
+                          S3BlobStoreConfiguration configuration,
+                          BlobId.Factory blobIdFactory,
+                          S3RequestOption s3RequestOption) {
         this.configuration = configuration;
         this.client = s3ClientFactory.get();
         this.blobIdFactory = blobIdFactory;
+        this.s3RequestOption = s3RequestOption;
 
         bucketNameResolver = BucketNameResolver.builder()
             .prefix(configuration.getBucketPrefix())
@@ -216,9 +225,20 @@ public class S3BlobStoreDAO implements BlobStoreDAO {
     }
 
     private Mono<GetObjectRequest.Builder> 
buildGetObjectRequestBuilder(BucketName bucketName, BlobId blobId) {
-        return Mono.just(GetObjectRequest.builder()
+        GetObjectRequest.Builder baseBuilder = GetObjectRequest.builder()
             .bucket(bucketName.asString())
-            .key(blobId.asString()));
+            .key(blobId.asString());
+
+        if (s3RequestOption.ssec().enable()) {
+            return 
Mono.from(s3RequestOption.ssec().sseCustomerKeyFactory().get()
+                    .generate(bucketName, blobId))
+                .map(sseCustomerKey -> baseBuilder
+                    .sseCustomerAlgorithm(sseCustomerKey.ssecAlgorithm())
+                    .sseCustomerKey(sseCustomerKey.customerKey())
+                    .sseCustomerKeyMD5(sseCustomerKey.md5()));
+        }
+
+        return Mono.just(baseBuilder);
     }
 
     @Override
@@ -279,10 +299,20 @@ public class S3BlobStoreDAO implements BlobStoreDAO {
     }
 
     private Mono<PutObjectRequest.Builder> 
buildPutObjectRequestBuilder(BucketName bucketName, long contentLength, BlobId 
blobId) {
-        return Mono.just(PutObjectRequest.builder()
+        PutObjectRequest.Builder baseBuilder = PutObjectRequest.builder()
             .bucket(bucketName.asString())
-            .contentLength(contentLength)
-            .key(blobId.asString()));
+            .key(blobId.asString())
+            .contentLength(contentLength);
+
+        if (s3RequestOption.ssec().enable()) {
+            return 
Mono.from(s3RequestOption.ssec().sseCustomerKeyFactory().get().generate(bucketName,
 blobId))
+                .map(sseCustomerKey -> baseBuilder
+                    .sseCustomerAlgorithm(sseCustomerKey.ssecAlgorithm())
+                    .sseCustomerKey(sseCustomerKey.customerKey())
+                    .sseCustomerKeyMD5(sseCustomerKey.md5()));
+        }
+
+        return Mono.just(baseBuilder);
     }
 
     private Flux<ByteBuffer> chunkStream(int chunkSize, InputStream stream) {
diff --git 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
new file mode 100644
index 0000000000..5de59834d2
--- /dev/null
+++ 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws;
+
+import java.util.Optional;
+
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECustomerKeyFactory;
+
+import com.google.common.base.Preconditions;
+
+public record S3RequestOption(SSEC ssec) {
+    static S3RequestOption DEFAULT = new 
S3RequestOption(S3RequestOption.SSEC.DISABLED);
+
+    public record SSEC(boolean enable, 
java.util.Optional<S3SSECustomerKeyFactory> sseCustomerKeyFactory) {
+        static S3RequestOption.SSEC DISABLED = new S3RequestOption.SSEC(false, 
Optional.empty());
+
+        public SSEC {
+            Preconditions.checkArgument(!enable || 
sseCustomerKeyFactory.isPresent(), "SSE Customer Key Factory must be present 
when SSE is enabled");
+        }
+    }
+}
diff --git 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
new file mode 100644
index 0000000000..9160fa8f70
--- /dev/null
+++ 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
@@ -0,0 +1,46 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.google.common.base.Preconditions;
+
+public interface S3SSECConfiguration {
+
+    String SSEC_ALGORITHM_DEFAULT = "AES256";
+    String CUSTOMER_KEY_FACTORY_ALGORITHM_DEFAULT = "PBKDF2WithHmacSHA256";
+    List<String> SUPPORTED_ALGORITHMS = List.of(SSEC_ALGORITHM_DEFAULT);
+
+    String ssecAlgorithm();
+
+    default Optional<String> customerKeyFactoryAlgorithm() {
+        return Optional.of(CUSTOMER_KEY_FACTORY_ALGORITHM_DEFAULT);
+    }
+
+    record Basic(String ssecAlgorithm,
+                 String masterPassword,
+                 String salt) implements S3SSECConfiguration {
+        public Basic {
+            
Preconditions.checkArgument(SUPPORTED_ALGORITHMS.contains(ssecAlgorithm), 
"Unsupported algorithm: " + ssecAlgorithm + ". The supported algorithms are: " 
+ SUPPORTED_ALGORITHMS);
+        }
+    }
+}
diff --git 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyFactory.java
 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyFactory.java
new file mode 100644
index 0000000000..f694051f92
--- /dev/null
+++ 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyFactory.java
@@ -0,0 +1,61 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BucketName;
+import org.reactivestreams.Publisher;
+
+import com.github.fge.lambdas.Throwing;
+
+import reactor.core.publisher.Mono;
+
+public interface S3SSECustomerKeyFactory {
+
+    record SSECustomerKey(String customerKey,
+                          String md5,
+                          String ssecAlgorithm) {
+    }
+
+    Publisher<SSECustomerKey> generate(BucketName bucketName, BlobId blobId);
+
+    class SingleCustomerKeyFactory implements S3SSECustomerKeyFactory {
+
+        private final SSECustomerKey sseCustomerKey;
+
+        public SingleCustomerKeyFactory(S3SSECConfiguration.Basic 
sseCustomerConfiguration) throws InvalidKeySpecException, 
NoSuchAlgorithmException {
+            S3SSECustomerKeyGenerator sseCustomerKeyGenerator = 
sseCustomerConfiguration.customerKeyFactoryAlgorithm()
+                .map(Throwing.function(S3SSECustomerKeyGenerator::new))
+                .orElseGet(Throwing.supplier(S3SSECustomerKeyGenerator::new));
+
+            String customerKey = 
sseCustomerKeyGenerator.generateCustomerKey(sseCustomerConfiguration.masterPassword(),
 sseCustomerConfiguration.salt());
+            String customerMd5 = 
sseCustomerKeyGenerator.generateCustomerKeyMd5(customerKey);
+            this.sseCustomerKey = new SSECustomerKey(customerKey, customerMd5, 
sseCustomerConfiguration.ssecAlgorithm());
+        }
+
+        @Override
+        public Mono<SSECustomerKey> generate(BucketName bucketName, BlobId 
blobId) {
+            return Mono.just(sseCustomerKey);
+        }
+    }
+}
diff --git 
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGenerator.java
 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGenerator.java
new file mode 100644
index 0000000000..d712ac8752
--- /dev/null
+++ 
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGenerator.java
@@ -0,0 +1,55 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+import com.google.common.hash.Hashing;
+
+public class S3SSECustomerKeyGenerator {
+
+    public static final int ITERATION_COUNT = 65536;
+    public static final int KEY_LENGTH = 256;
+
+    private final SecretKeyFactory secretKeyFactory;
+
+    public S3SSECustomerKeyGenerator() throws NoSuchAlgorithmException {
+        this(S3SSECConfiguration.CUSTOMER_KEY_FACTORY_ALGORITHM_DEFAULT);
+    }
+
+    public S3SSECustomerKeyGenerator(String customerKeyFactoryAlgorithm) 
throws NoSuchAlgorithmException {
+        this.secretKeyFactory = 
SecretKeyFactory.getInstance(customerKeyFactoryAlgorithm);
+    }
+
+    public String generateCustomerKey(String masterPassword, String salt) 
throws InvalidKeySpecException {
+        PBEKeySpec spec = new PBEKeySpec(masterPassword.toCharArray(), 
salt.getBytes(), ITERATION_COUNT, KEY_LENGTH);
+        byte[] derivedKey = secretKeyFactory.generateSecret(spec).getEncoded();
+        return Base64.getEncoder().encodeToString(derivedKey);
+    }
+
+    public String generateCustomerKeyMd5(String customerKey) {
+        return 
Base64.getEncoder().encodeToString(Hashing.md5().hashBytes(Base64.getDecoder().decode(customerKey)).asBytes());
+    }
+}
\ No newline at end of file
diff --git 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3BlobStoreDAOWithSSECTest.java
 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3BlobStoreDAOWithSSECTest.java
new file mode 100644
index 0000000000..4c55a699fa
--- /dev/null
+++ 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3BlobStoreDAOWithSSECTest.java
@@ -0,0 +1,120 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BUCKET_NAME;
+import static 
org.apache.james.blob.objectstorage.aws.JamesS3MetricPublisher.DEFAULT_S3_METRICS_PREFIX;
+import static 
org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration.UPLOAD_RETRY_EXCEPTION_PREDICATE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BlobStoreDAOContract;
+import org.apache.james.blob.api.TestBlobId;
+import org.apache.james.blob.objectstorage.aws.JamesS3MetricPublisher;
+import org.apache.james.blob.objectstorage.aws.Region;
+import org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration;
+import org.apache.james.blob.objectstorage.aws.S3BlobStoreDAO;
+import org.apache.james.blob.objectstorage.aws.S3ClientFactory;
+import org.apache.james.blob.objectstorage.aws.S3MinioExtension;
+import org.apache.james.blob.objectstorage.aws.S3RequestOption;
+import org.apache.james.metrics.api.NoopGaugeRegistry;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import reactor.core.publisher.Flux;
+import reactor.util.retry.Retry;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+
+public class S3BlobStoreDAOWithSSECTest implements BlobStoreDAOContract, 
S3SSECContract {
+
+    @RegisterExtension
+    static S3MinioExtension minoExtension = new S3MinioExtension();
+
+    private static S3BlobStoreDAO testee;
+    private static S3ClientFactory s3ClientFactory;
+
+    @BeforeAll
+    static void setUp() throws Exception {
+        S3BlobStoreConfiguration s3Configuration = 
S3BlobStoreConfiguration.builder()
+            
.authConfiguration(minoExtension.minioDocker().getAwsS3AuthConfiguration())
+            
.region(Region.of(software.amazon.awssdk.regions.Region.EU_WEST_1.id()))
+            .uploadRetrySpec(Optional.of(Retry.backoff(3, 
java.time.Duration.ofSeconds(1))
+                .filter(UPLOAD_RETRY_EXCEPTION_PREDICATE)))
+            .build();
+
+        s3ClientFactory = new S3ClientFactory(s3Configuration, () -> new 
JamesS3MetricPublisher(new RecordingMetricFactory(), new NoopGaugeRegistry(),
+            DEFAULT_S3_METRICS_PREFIX));
+
+        S3SSECustomerKeyFactory sseCustomerKeyFactory = new 
S3SSECustomerKeyFactory.SingleCustomerKeyFactory(new 
S3SSECConfiguration.Basic("AES256", "masterPassword", "salt"));
+
+        S3RequestOption s3RequestOption = new S3RequestOption(new 
S3RequestOption.SSEC(true, Optional.of(sseCustomerKeyFactory)));
+        testee = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, new 
TestBlobId.Factory(), s3RequestOption);
+    }
+
+    @BeforeEach
+    void beforeEach() throws Exception {
+        // Why? 
https://github.com/apache/james-project/pull/1981#issuecomment-2380396460
+        s3ClientFactory.get().createBucket(builder -> 
builder.bucket(TEST_BUCKET_NAME.asString()))
+            .get();
+    }
+
+    @Override
+    public BlobStoreDAO testee() {
+        return testee;
+    }
+
+    @Override
+    public S3AsyncClient s3Client() {
+        return s3ClientFactory.get();
+    }
+
+    private void deleteBucket(String bucketName) {
+        try {
+            s3ClientFactory.get().deleteBucket(builder -> 
builder.bucket(bucketName))
+                .get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new RuntimeException("Error while deleting bucket", e);
+        }
+    }
+
+    @Test
+    @Override
+    public void listBucketsShouldReturnEmptyWhenNone() {
+        deleteBucket(TEST_BUCKET_NAME.asString());
+
+        BlobStoreDAO store = testee();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    @Override
+    @Disabled("S3minio return `Connection: close` in header response, 
https://github.com/apache/james-project/pull/1981#issuecomment-2380396460";)
+    public void deleteBucketConcurrentlyShouldNotFail() {
+    }
+}
diff --git 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECContract.java
 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECContract.java
new file mode 100644
index 0000000000..f18033746f
--- /dev/null
+++ 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECContract.java
@@ -0,0 +1,89 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import static org.apache.james.blob.api.BlobStoreDAOFixture.SHORT_BYTEARRAY;
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BLOB_ID;
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BUCKET_NAME;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.junit.jupiter.api.Test;
+
+import reactor.core.publisher.Mono;
+import software.amazon.awssdk.core.BytesWrapper;
+import software.amazon.awssdk.core.async.AsyncResponseTransformer;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+
+public interface S3SSECContract {
+
+    BlobStoreDAO testee();
+
+    S3AsyncClient s3Client();
+
+    @Test
+    default void getObjectShouldFailWhenNotDefineCustomerKeyInHeaderRequest() {
+        Mono.from(testee().save(TEST_BUCKET_NAME, TEST_BLOB_ID, 
SHORT_BYTEARRAY)).block();
+
+        assertThatThrownBy(() -> 
s3Client().getObject(GetObjectRequest.builder()
+                .bucket(TEST_BUCKET_NAME.asString())
+                .key(TEST_BLOB_ID.asString())
+                .build(), AsyncResponseTransformer.toBytes())
+            .thenApply(BytesWrapper::asByteArray)
+            .get())
+            .hasMessageContaining("The object was stored using a form of 
Server Side Encryption");
+    }
+
+    @Test
+    default void getObjectShouldFailWhenInvalidCustomerKeyInRequest() {
+        Mono.from(testee().save(TEST_BUCKET_NAME, TEST_BLOB_ID, 
SHORT_BYTEARRAY)).block();
+
+        assertThatThrownBy(() -> 
s3Client().getObject(GetObjectRequest.builder()
+                .bucket(TEST_BUCKET_NAME.asString())
+                .key(TEST_BLOB_ID.asString())
+                .sseCustomerKey("invalid")
+                .sseCustomerKeyMD5("123")
+                .build(), AsyncResponseTransformer.toBytes())
+            .thenApply(BytesWrapper::asByteArray)
+            .get())
+            .hasMessageContaining("Requests specifying Server Side Encryption 
with Customer provided keys must provide a valid encryption algorithm");
+    }
+
+    @Test
+    default void getObjectShouldFailWhenNotMatchedCustomerKeyInRequest() 
throws Exception {
+        Mono.from(testee().save(TEST_BUCKET_NAME, TEST_BLOB_ID, 
SHORT_BYTEARRAY)).block();
+        S3SSECustomerKeyGenerator sseCustomerKeyGenerator = new 
S3SSECustomerKeyGenerator();
+
+        String customerKey = 
sseCustomerKeyGenerator.generateCustomerKey("random1", "salt123random");
+        String customerKeyMd5 = 
sseCustomerKeyGenerator.generateCustomerKeyMd5(customerKey);
+
+        assertThatThrownBy(() -> 
s3Client().getObject(GetObjectRequest.builder()
+                .bucket(TEST_BUCKET_NAME.asString())
+                .key(TEST_BLOB_ID.asString())
+                .sseCustomerKey(customerKey)
+                .sseCustomerKeyMD5(customerKeyMd5)
+                .sseCustomerAlgorithm("AES256")
+                .build(), AsyncResponseTransformer.toBytes())
+            .thenApply(BytesWrapper::asByteArray)
+            .get())
+            .hasMessageContaining("Access Denied");
+    }
+}
diff --git 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGeneratorTest.java
 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGeneratorTest.java
new file mode 100644
index 0000000000..27705798cc
--- /dev/null
+++ 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECustomerKeyGeneratorTest.java
@@ -0,0 +1,61 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import java.time.Duration;
+import java.util.Map;
+
+import org.apache.james.util.concurrency.ConcurrentTestRunner;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class S3SSECustomerKeyGeneratorTest {
+    record SampleTest(String masterPassword, String salt, String 
expectedCustomerKey) {
+    }
+
+    static Map<Integer, SampleTest> SAMPLE_TEST = ImmutableMap.of(
+        1, new SampleTest("test1", "salt1", 
"Le39M/h9Wi72qMMV9O4ibqZxWb9cL3rQRUy0rfMdODc="),
+        2, new SampleTest("test2", "salt2", 
"Yo8T24EXwAZm3lyGyzfiyYx0FcSdK5ai9pmgO6yLDe8="),
+        3, new SampleTest("test3", "salt3", 
"KGMvLVMB0Y7U2WpYiQmbWn+g9fE3ZvH0pSrx1BzNups="),
+        4, new SampleTest("test4", "salt4", 
"kGbGrfAgfAjkEByylQoFO77yR0JeSA44fhnkuREdrkY="),
+        5, new SampleTest("test5", "salt5", 
"dxaGCxA7c7bmG2gEjHkfLMvH6SFUQp0d0wa0xKuia3Q="),
+        6, new SampleTest("test6", "salt6", 
"GhNsbP2E/2uSMpgD59jR1W8oTmoRwZ/bo1ScSqh4wiQ="),
+        7, new SampleTest("test7", "salt7", 
"X2VE+qpQnTFnWN5e/SgQyoyBXfxVM3Oy2+2f8Auba6o="),
+        8, new SampleTest("test8", "salt8", 
"EVz2OnBEOyfuH4h4p5X7utZVJ+LXAojnti102Wzp6Lc="),
+        9, new SampleTest("test9", "salt9", 
"HNA8IazLNDFGATKjBjzeNJPZ6fn9/g19rzIFAHYVbGs="),
+        10, new SampleTest("test10", "salt10", 
"8R4WuFhTDpRMsQjht6XMrq4CFODC9oRZ/zucRXZ9+JU="));
+
+    @Test
+    void generateShouldWorkCorrectWhenConcurrent() throws Exception {
+        S3SSECustomerKeyGenerator testee = new 
S3SSECustomerKeyGenerator("PBKDF2WithHmacSHA256");
+        ConcurrentTestRunner.builder()
+            .operation(((threadNumber, step) -> {
+                int index = threadNumber + 1;
+                String customerKey = testee.generateCustomerKey("test" + 
index, "salt" + index);
+                
assertThat(customerKey).isEqualTo(SAMPLE_TEST.get(index).expectedCustomerKey());
+            }))
+            .threadCount(10)
+            .operationCount(100)
+            .runSuccessfullyWithin(Duration.ofMinutes(1));
+    }
+}
diff --git 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/SingleCustomerKeyFactoryTest.java
 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/SingleCustomerKeyFactoryTest.java
new file mode 100644
index 0000000000..08f878a1f0
--- /dev/null
+++ 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/sse/SingleCustomerKeyFactoryTest.java
@@ -0,0 +1,65 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws.sse;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.TestBlobId;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import reactor.core.publisher.Mono;
+
+public class SingleCustomerKeyFactoryTest {
+    private static S3SSECustomerKeyFactory.SingleCustomerKeyFactory testee;
+
+    @BeforeAll
+    static void setup() throws InvalidKeySpecException, 
NoSuchAlgorithmException {
+        testee = new S3SSECustomerKeyFactory.SingleCustomerKeyFactory(new 
S3SSECConfiguration.Basic("AES256", "masterPassword", "salt"));
+
+    }
+
+    @Test
+    void generateShouldReturnSSECustomerKey() {
+        // When
+        S3SSECustomerKeyFactory.SSECustomerKey sseCustomerKey = 
Mono.from(testee.generate(BucketName.of("bucket1"), new 
TestBlobId("blobId"))).block();
+
+        // Then
+        
assertThat(sseCustomerKey.customerKey()).isEqualTo("tsY5n0n10fcpjm7aGArqbw231ptLGAF5ubxMxI2+QYw=");
+        assertThat(sseCustomerKey.md5()).isEqualTo("G6IB1YLHiY/9uCh3TJhxzQ==");
+        assertThat(sseCustomerKey.ssecAlgorithm()).isEqualTo("AES256");
+    }
+
+    @Test
+    void generateShouldReturnSameSSECustomerKey() {
+        // When
+        S3SSECustomerKeyFactory.SSECustomerKey sseCustomerKey1 = 
Mono.from(testee.generate(BucketName.of("bucket1"), new 
TestBlobId("blobId"))).block();
+        S3SSECustomerKeyFactory.SSECustomerKey sseCustomerKey2 = 
Mono.from(testee.generate(BucketName.of("bucket1"), new 
TestBlobId("blobId"))).block();
+        S3SSECustomerKeyFactory.SSECustomerKey sseCustomerKey3 = 
Mono.from(testee.generate(BucketName.of("bucket3"), new 
TestBlobId("blobId3"))).block();
+
+        // Then
+        assertThat(sseCustomerKey1).isEqualTo(sseCustomerKey2)
+            .isEqualTo(sseCustomerKey3);
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to