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