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 <[email protected]> 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: [email protected] For additional commands, e-mail: [email protected]
