You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/04/02 01:34:30 UTC

[james-project] 03/08: JAMES-3524 Write a BlobStoreDAO wrapper performing AES encryption

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

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

commit 289ccbb0f425170f58cc3db1eab712b0fdf013da
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 25 14:56:32 2021 +0700

    JAMES-3524 Write a BlobStoreDAO wrapper performing AES encryption
    
    Uses byte array as an intermediate data structure...
    
    Inspiration: Jean Helou, AESPayloadCodec
    
    https://github.com/apache/james-project/blame/james-project-3.5.0/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/AESPayloadCodec.java
---
 server/blob/blob-aes/pom.xml                       |  12 ++
 .../org/apache/james/blob/aes/AESBlobStoreDAO.java | 155 +++++++++++++++++++++
 .../apache/james/blob/aes/AESBlobStoreDAOTest.java |  86 ++++++++++++
 3 files changed, 253 insertions(+)

diff --git a/server/blob/blob-aes/pom.xml b/server/blob/blob-aes/pom.xml
index dc1cf0a..c67ff33 100644
--- a/server/blob/blob-aes/pom.xml
+++ b/server/blob/blob-aes/pom.xml
@@ -53,6 +53,10 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>com.github.fge</groupId>
+            <artifactId>throwing-lambdas</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.google.crypto.tink</groupId>
             <artifactId>tink</artifactId>
             <version>1.5.0</version>
@@ -61,6 +65,14 @@
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-core</artifactId>
+        </dependency>
     </dependencies>
 
 
diff --git a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
new file mode 100644
index 0000000..3d39278
--- /dev/null
+++ b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
@@ -0,0 +1,155 @@
+/****************************************************************
+ * 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.aes;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.ObjectNotFoundException;
+import org.apache.james.blob.api.ObjectStoreIOException;
+import org.reactivestreams.Publisher;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteSource;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.crypto.tink.subtle.AesGcmJce;
+
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+public class AESBlobStoreDAO implements BlobStoreDAO {
+    private static final byte[] EMPTY_ASSOCIATED_DATA = new byte[0];
+    private static final int PBKDF2_ITERATIONS = 65536;
+    private static final int KEY_SIZE = 256;
+    private static final String SECRET_KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA256";
+
+    private final BlobStoreDAO underlying;
+    private final Aead aead;
+
+    public AESBlobStoreDAO(BlobStoreDAO underlying, CryptoConfig cryptoConfig) {
+        this.underlying = underlying;
+
+        try {
+            AeadConfig.register();
+
+            SecretKey secretKey = deriveKey(cryptoConfig);
+            aead = new AesGcmJce(secretKey.getEncoded());
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException("Error while starting AESPayloadCodec", e);
+        }
+    }
+
+    private static SecretKey deriveKey(CryptoConfig cryptoConfig) throws NoSuchAlgorithmException, InvalidKeySpecException {
+        byte[] saltBytes = cryptoConfig.salt();
+        SecretKeyFactory skf = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_ALGORITHM);
+        PBEKeySpec spec = new PBEKeySpec(cryptoConfig.password(), saltBytes, PBKDF2_ITERATIONS, KEY_SIZE);
+        return skf.generateSecret(spec);
+    }
+
+    public byte[] encrypt(byte[] input) {
+        try {
+            return aead.encrypt(input, EMPTY_ASSOCIATED_DATA);
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException("Unable to build payload for object storage, failed to encrypt", e);
+        }
+    }
+
+    public byte[] decrypt(byte[] ciphertext) throws IOException {
+        try {
+            return aead.decrypt(ciphertext, EMPTY_ASSOCIATED_DATA);
+        } catch (GeneralSecurityException e) {
+            throw new IOException("Incorrect crypto setup", e);
+        }
+    }
+
+    @Override
+    public InputStream read(BucketName bucketName, BlobId blobId) throws ObjectStoreIOException, ObjectNotFoundException {
+        return Mono.from(underlying.readBytes(bucketName, blobId))
+            .map(Throwing.function(this::decrypt))
+            .map(ByteArrayInputStream::new)
+            .subscribeOn(Schedulers.elastic())
+            .block();
+    }
+
+    @Override
+    public Publisher<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
+        return Mono.from(underlying.readBytes(bucketName, blobId))
+            .map(Throwing.function(this::decrypt));
+    }
+
+    @Override
+    public Publisher<Void> save(BucketName bucketName, BlobId blobId, byte[] data) {
+        Preconditions.checkNotNull(bucketName);
+        Preconditions.checkNotNull(blobId);
+        Preconditions.checkNotNull(data);
+
+        return Mono.just(data)
+            .flatMap(payload -> Mono.fromCallable(() -> encrypt(payload)).subscribeOn(Schedulers.parallel()))
+            .flatMap(encryptedPayload -> Mono.from(underlying.save(bucketName, blobId, encryptedPayload)))
+            .onErrorMap(e -> new ObjectStoreIOException("Exception occurred while saving bytearray", e));
+    }
+
+    @Override
+    public Publisher<Void> save(BucketName bucketName, BlobId blobId, InputStream inputStream) {
+        Preconditions.checkNotNull(bucketName);
+        Preconditions.checkNotNull(blobId);
+        Preconditions.checkNotNull(inputStream);
+
+        return Mono.just(inputStream)
+            .flatMap(data -> Mono.fromCallable(() -> IOUtils.toByteArray(inputStream)).subscribeOn(Schedulers.parallel()))
+            .flatMap(encryptedData -> Mono.from(save(bucketName, blobId, encryptedData)))
+            .onErrorMap(e -> new ObjectStoreIOException("Exception occurred while saving bytearray", e));
+    }
+
+    @Override
+    public Publisher<Void> save(BucketName bucketName, BlobId blobId, ByteSource content) {
+        Preconditions.checkNotNull(bucketName);
+        Preconditions.checkNotNull(blobId);
+        Preconditions.checkNotNull(content);
+
+        return Mono.using(content::openStream,
+            in -> Mono.from(save(bucketName, blobId, in)),
+            Throwing.consumer(InputStream::close));
+    }
+
+    @Override
+    public Publisher<Void> delete(BucketName bucketName, BlobId blobId) {
+        return underlying.delete(bucketName, blobId);
+    }
+
+    @Override
+    public Publisher<Void> deleteBucket(BucketName bucketName) {
+        return underlying.deleteBucket(bucketName);
+    }
+}
diff --git a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
new file mode 100644
index 0000000..8be3728
--- /dev/null
+++ b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
@@ -0,0 +1,86 @@
+/****************************************************************
+ * 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.aes;
+
+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.assertThat;
+
+import java.io.ByteArrayInputStream;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BlobStoreDAOContract;
+import org.apache.james.blob.memory.MemoryBlobStoreDAO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.io.ByteSource;
+
+import reactor.core.publisher.Mono;
+
+class AESBlobStoreDAOTest implements BlobStoreDAOContract {
+    private static final String SAMPLE_SALT = "c603a7327ee3dcbc031d8d34b1096c605feca5e1";
+    private static final CryptoConfig CRYPTO_CONFIG = CryptoConfig.builder()
+        .salt(SAMPLE_SALT)
+        .password("testing".toCharArray())
+        .build();
+
+    private AESBlobStoreDAO testee;
+    private MemoryBlobStoreDAO underlying;
+
+    @BeforeEach
+    void setUp() {
+        underlying = new MemoryBlobStoreDAO();
+        testee = new AESBlobStoreDAO(underlying, CRYPTO_CONFIG);
+    }
+
+    @Override
+    public BlobStoreDAO testee() {
+        return testee;
+    }
+
+    @Test
+    void underlyingDataShouldBeEncrypted() {
+        Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        byte[] bytes = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+        assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
+    }
+
+    @Test
+    void underlyingDataShouldBeEncryptedWhenUsingStream() {
+        Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, new ByteArrayInputStream(SHORT_BYTEARRAY))).block();
+
+        byte[] bytes = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+        assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
+    }
+
+    @Test
+    void underlyingDataShouldBeEncryptedWhenUsingByteSource() {
+        Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ByteSource.wrap(SHORT_BYTEARRAY))).block();
+
+        byte[] bytes = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+        assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
+    }
+}
\ No newline at end of file

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