JAMES-2525 adds encryption capability to the object storage The capability configurability is fairly limited at this point, it will use the authenticated encryption AES256/GCM/NoPadding. The 256 bits key is derived from a provided salt and password using PBKDF2 with the maximum possible iterations of PBKDF2WithHmacSHA1.
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/3d1981d3 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/3d1981d3 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/3d1981d3 Branch: refs/heads/master Commit: 3d1981d3aa505e1806477a8d8db80c461cf25866 Parents: eccd6a2 Author: Jean Helou <[email protected]> Authored: Thu Sep 13 11:39:18 2018 +0200 Committer: Benoit Tellier <[email protected]> Committed: Wed Oct 31 08:48:47 2018 +0700 ---------------------------------------------------------------------- server/blob/blob-objectstorage/pom.xml | 5 ++ .../blob/objectstorage/AESPayloadCodec.java | 80 +++++++++++++++++ .../blob/objectstorage/DefaultPayloadCodec.java | 38 ++++++++ .../objectstorage/ObjectStorageBlobsDAO.java | 12 ++- .../ObjectStorageBlobsDAOBuilder.java | 15 +++- .../james/blob/objectstorage/PayloadCodec.java | 33 +++++++ .../blob/objectstorage/crypto/CryptoConfig.java | 45 ++++++++++ .../crypto/CryptoConfigBuilder.java | 48 +++++++++++ .../objectstorage/crypto/CryptoException.java | 34 ++++++++ .../crypto/PBKDF2StreamingAeadFactory.java | 59 +++++++++++++ .../blob/objectstorage/AESPayloadCodecTest.java | 91 ++++++++++++++++++++ .../objectstorage/DefaultPayloadCodecTest.java | 52 +++++++++++ .../ObjectStorageBlobsDAOTest.java | 53 +++++++++++- .../objectstorage/PayloadCodecContract.java | 46 ++++++++++ .../crypto/CryptoConfigBuilderTest.java | 82 ++++++++++++++++++ 15 files changed, 684 insertions(+), 9 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/pom.xml ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/pom.xml b/server/blob/blob-objectstorage/pom.xml index 3b895b5..972622c 100644 --- a/server/blob/blob-objectstorage/pom.xml +++ b/server/blob/blob-objectstorage/pom.xml @@ -62,6 +62,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>com.google.crypto.tink</groupId> + <artifactId>tink</artifactId> + <version>1.2.0</version> + </dependency> + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/AESPayloadCodec.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/AESPayloadCodec.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/AESPayloadCodec.java new file mode 100644 index 0000000..ee60dd4 --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/AESPayloadCodec.java @@ -0,0 +1,80 @@ +/**************************************************************** + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.security.GeneralSecurityException; + +import org.apache.commons.io.IOUtils; +import org.apache.james.blob.objectstorage.crypto.CryptoConfig; +import org.apache.james.blob.objectstorage.crypto.PBKDF2StreamingAeadFactory; +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.crypto.tink.subtle.AesGcmHkdfStreaming; + +public class AESPayloadCodec implements PayloadCodec { + private static final Logger LOGGER = LoggerFactory.getLogger(AESPayloadCodec.class); + private final AesGcmHkdfStreaming streamingAead; + + public AESPayloadCodec(CryptoConfig cryptoConfig) { + streamingAead = PBKDF2StreamingAeadFactory.newAesGcmHkdfStreaming(cryptoConfig); + } + + @Override + public Payload write(InputStream is) { + PipedInputStream snk = new PipedInputStream(); + try { + PipedOutputStream src = new PipedOutputStream(snk); + OutputStream outputStream = streamingAead.newEncryptingStream(src, PBKDF2StreamingAeadFactory.EMPTY_ASSOCIATED_DATA); + Thread copyThread = new Thread(() -> { + try (OutputStream stream = outputStream) { + IOUtils.copy(is, stream); + } catch (IOException e) { + throw new RuntimeException("Stream copy failure ", e); + } + }); + copyThread.setUncaughtExceptionHandler((Thread t, Throwable e) -> + LOGGER.error("Unable to encrypt payload's input stream",e) + ); + copyThread.start(); + return Payloads.newInputStreamPayload(snk); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException("Unable to build payload for object storage, failed to " + + "encrypt", e); + } + } + + @Override + public InputStream read(Payload payload) throws IOException { + try { + return streamingAead.newDecryptingStream(payload.openStream(), PBKDF2StreamingAeadFactory.EMPTY_ASSOCIATED_DATA); + } catch (GeneralSecurityException e) { + throw new IOException("Incorrect crypto setup", e); + } + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/DefaultPayloadCodec.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/DefaultPayloadCodec.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/DefaultPayloadCodec.java new file mode 100644 index 0000000..bd56c49 --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/DefaultPayloadCodec.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; + +import java.io.IOException; +import java.io.InputStream; + +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; + +public class DefaultPayloadCodec implements PayloadCodec { + @Override + public Payload write(InputStream is) { + return Payloads.newInputStreamPayload(is); + } + + @Override + public InputStream read(Payload payload) throws IOException { + return payload.openStream(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAO.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAO.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAO.java index 7422b55..81fb3be 100644 --- a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAO.java +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAO.java @@ -34,6 +34,7 @@ import org.apache.james.blob.objectstorage.swift.SwiftTempAuthObjectStorage; import org.jclouds.blobstore.domain.Blob; import org.jclouds.blobstore.options.CopyOptions; import org.jclouds.domain.Location; +import org.jclouds.io.Payload; import com.github.fge.lambdas.Throwing; import com.google.common.base.Preconditions; @@ -49,12 +50,14 @@ public class ObjectStorageBlobsDAO implements BlobStore { private final ContainerName containerName; private final org.jclouds.blobstore.BlobStore blobStore; + private final PayloadCodec payloadCodec; ObjectStorageBlobsDAO(ContainerName containerName, BlobId.Factory blobIdFactory, - org.jclouds.blobstore.BlobStore blobStore) { + org.jclouds.blobstore.BlobStore blobStore, PayloadCodec payloadCodec) { this.blobIdFactory = blobIdFactory; this.containerName = containerName; this.blobStore = blobStore; + this.payloadCodec = payloadCodec; } public static ObjectStorageBlobsDAOBuilder builder(SwiftTempAuthObjectStorage.Configuration testConfig) { @@ -106,7 +109,8 @@ public class ObjectStorageBlobsDAO implements BlobStore { private BlobId save(InputStream data, BlobId id) { String containerName = this.containerName.value(); HashingInputStream hashingInputStream = new HashingInputStream(Hashing.sha256(), data); - Blob blob = blobStore.blobBuilder(id.asString()).payload(hashingInputStream).build(); + Payload payload = payloadCodec.write(hashingInputStream); + Blob blob = blobStore.blobBuilder(id.asString()).payload(payload).build(); blobStore.putBlob(containerName, blob); return blobIdFactory.from(hashingInputStream.hash().toString()); } @@ -123,13 +127,13 @@ public class ObjectStorageBlobsDAO implements BlobStore { try { if (blob != null) { - return blob.getPayload().openStream(); + return payloadCodec.read(blob.getPayload()); } else { return EMPTY_STREAM; } } catch (IOException cause) { throw new ObjectStoreException( - "Failed to read blob " + blobId.asString(), + "Failed to readBytes blob " + blobId.asString(), cause); } http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOBuilder.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOBuilder.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOBuilder.java index 1f1f0e7..535741a 100644 --- a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOBuilder.java +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOBuilder.java @@ -19,6 +19,7 @@ package org.apache.james.blob.objectstorage; +import java.util.Optional; import java.util.function.Supplier; import org.apache.james.blob.api.BlobId; @@ -31,8 +32,10 @@ public class ObjectStorageBlobsDAOBuilder { private final Supplier<BlobStore> supplier; private ContainerName containerName; private BlobId.Factory blobIdFactory; + private Optional<PayloadCodec> payloadCodec; public ObjectStorageBlobsDAOBuilder(Supplier<BlobStore> supplier) { + this.payloadCodec = Optional.empty(); this.supplier = supplier; } @@ -46,10 +49,20 @@ public class ObjectStorageBlobsDAOBuilder { return this; } + public ObjectStorageBlobsDAOBuilder payloadCodec(PayloadCodec payloadCodec) { + this.payloadCodec = Optional.of(payloadCodec); + return this; + } + public ObjectStorageBlobsDAOBuilder payloadCodec(Optional<PayloadCodec> payloadCodec) { + this.payloadCodec = payloadCodec; + return this; + } + public ObjectStorageBlobsDAO build() { Preconditions.checkState(containerName != null); Preconditions.checkState(blobIdFactory != null); - return new ObjectStorageBlobsDAO(containerName, blobIdFactory, supplier.get()); + + return new ObjectStorageBlobsDAO(containerName, blobIdFactory, supplier.get(), payloadCodec.orElse(PayloadCodec.DEFAULT_CODEC)); } @VisibleForTesting http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/PayloadCodec.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/PayloadCodec.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/PayloadCodec.java new file mode 100644 index 0000000..5e1cfc5 --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/PayloadCodec.java @@ -0,0 +1,33 @@ +/**************************************************************** + * 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; + +import java.io.IOException; +import java.io.InputStream; + +import org.jclouds.io.Payload; + +public interface PayloadCodec { + Payload write(InputStream is); + + InputStream read(Payload payload) throws IOException; + + PayloadCodec DEFAULT_CODEC = new DefaultPayloadCodec(); +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfig.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfig.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfig.java new file mode 100644 index 0000000..ba38f53 --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfig.java @@ -0,0 +1,45 @@ +/**************************************************************** + * 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.crypto; + +import com.google.crypto.tink.subtle.Hex; + +public class CryptoConfig { + + public static CryptoConfigBuilder builder() { + return new CryptoConfigBuilder(); + } + + private final String salt; + private final char[] password; + + public CryptoConfig(String salt, char[] password) { + this.salt = salt; + this.password = password; + } + + public byte[] salt() { + return Hex.decode(salt); + } + + public char[] password() { + return password; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilder.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilder.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilder.java new file mode 100644 index 0000000..d19e0ad --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilder.java @@ -0,0 +1,48 @@ +/**************************************************************** + * 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.crypto; + +import com.amazonaws.util.StringUtils; +import com.google.common.base.Preconditions; +import com.google.crypto.tink.subtle.Hex; + +public class CryptoConfigBuilder { + private String salt; + private char[] password; + + CryptoConfigBuilder() { + } + + public CryptoConfigBuilder salt(String salt) { + this.salt = salt; + return this; + } + + public CryptoConfigBuilder password(char[] password) { + this.password = password; + return this; + } + + public CryptoConfig build() { + Preconditions.checkState(!StringUtils.isNullOrEmpty(salt)); + Preconditions.checkState(password != null && password.length > 0); + return new CryptoConfig(Hex.encode(Hex.decode(salt)), password); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoException.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoException.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoException.java new file mode 100644 index 0000000..70d351f --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/CryptoException.java @@ -0,0 +1,34 @@ +/**************************************************************** + * 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.crypto; + +public class CryptoException extends RuntimeException { + public CryptoException() { + super(); + } + + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/PBKDF2StreamingAeadFactory.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/PBKDF2StreamingAeadFactory.java b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/PBKDF2StreamingAeadFactory.java new file mode 100644 index 0000000..4611121 --- /dev/null +++ b/server/blob/blob-objectstorage/src/main/java/org/apache/james/blob/objectstorage/crypto/PBKDF2StreamingAeadFactory.java @@ -0,0 +1,59 @@ +/**************************************************************** + * 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.crypto; + +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 com.google.crypto.tink.subtle.AesGcmHkdfStreaming; + +public class PBKDF2StreamingAeadFactory { + private static final int PBKDF2_ITERATIONS = 65536; + private static final int KEY_SIZE = 256; + private static final String SECRET_KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1"; + private static final String HKDF_ALGO = "HmacSha256"; + private static final int KEY_SIZE_IN_BYTES = 32; + private static final int SEGMENT_SIZE = 4096; + private static final int OFFSET = 0; + public static final byte[] EMPTY_ASSOCIATED_DATA = new byte[0]; + + public static AesGcmHkdfStreaming newAesGcmHkdfStreaming(CryptoConfig config) { + try { + SecretKey secretKey = deriveKey(config); + return new AesGcmHkdfStreaming(secretKey.getEncoded(), HKDF_ALGO, KEY_SIZE_IN_BYTES, SEGMENT_SIZE, OFFSET); + } catch (GeneralSecurityException e) { + throw new CryptoException("Incorrect crypto setup", 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); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/AESPayloadCodecTest.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/AESPayloadCodecTest.java b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/AESPayloadCodecTest.java new file mode 100644 index 0000000..62b7507 --- /dev/null +++ b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/AESPayloadCodecTest.java @@ -0,0 +1,91 @@ +/**************************************************************** + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.james.blob.objectstorage.crypto.CryptoConfig; +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import com.google.crypto.tink.subtle.Hex; + +class AESPayloadCodecTest implements PayloadCodecContract { + private static final byte[] ENCRYPTED_BYTES = Hex.decode("28cdfb53c283185598ec7c49c415c6b56e85d3d74af89740270c2d2cd8006e1265a301436d919ed7acfc14586b5bd193e34c744ef1641230457dae3475"); + + @Override + public PayloadCodec codec() { + return new AESPayloadCodec( + new CryptoConfig( + "c603a7327ee3dcbc031d8d34b1096c605feca5e1", + "foobar".toCharArray())); + } + + @Test + void aesCodecShouldEncryptPayloadContentWhenWriting() throws Exception { + Payload payload = codec().write(expected()); + byte[] bytes = IOUtils.toByteArray(payload.openStream()); + // authenticated encryption uses a random salt for the authentication + // header all we can say for sure is that the output is not the same as + // the input. + assertThat(bytes).isNotEqualTo(SOME_BYTES); + } + + @Test + void aesCodecShouldDecryptPayloadContentWhenReading() throws Exception { + Payload payload = Payloads.newInputStreamPayload(new ByteArrayInputStream(ENCRYPTED_BYTES)); + + InputStream actual = codec().read(payload); + + assertThat(actual).hasSameContentAs(expected()); + } + + @Test + void aesCodecShouldRaiseExceptionWhenUnderliyingInputStreamFails() throws Exception { + Payload payload = + Payloads.newInputStreamPayload(new FilterInputStream(new ByteArrayInputStream(ENCRYPTED_BYTES)) { + private int readCount = 0; + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + if (readCount >= ENCRYPTED_BYTES.length / 2) { + throw new IOException(); + } else { + readCount += len; + return super.read(b, off, len); + } + } + }); + int i = ENCRYPTED_BYTES.length / 2; + byte[] bytes = new byte[i]; + InputStream is = codec().read(payload); + assertThatThrownBy(() -> is.read(bytes, 0, i)).isInstanceOf(IOException.class); + + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/DefaultPayloadCodecTest.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/DefaultPayloadCodecTest.java b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/DefaultPayloadCodecTest.java new file mode 100644 index 0000000..eed9869 --- /dev/null +++ b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/DefaultPayloadCodecTest.java @@ -0,0 +1,52 @@ +/**************************************************************** + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; + +import org.jclouds.io.Payload; +import org.jclouds.io.Payloads; +import org.junit.jupiter.api.Test; + +class DefaultPayloadCodecTest implements PayloadCodecContract { + @Override + public PayloadCodec codec() { + return new DefaultPayloadCodec(); + } + + @Test + void defaultCodecShouldNotChangePayloadContentWhenWriting() throws Exception { + Payload payload = codec().write(expected()); + + assertThat(payload.openStream()).hasSameContentAs(expected()); + } + + @Test + void defaultCodecShouldNotChangePayloadContentWhenReading() throws Exception { + Payload payload = Payloads.newInputStreamPayload(expected()); + + InputStream actual = codec().read(payload); + + assertThat(actual).hasSameContentAs(expected()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOTest.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOTest.java b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOTest.java index d5e101d..9a66875 100644 --- a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOTest.java +++ b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/ObjectStorageBlobsDAOTest.java @@ -22,13 +22,18 @@ package org.apache.james.blob.objectstorage; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.UUID; +import org.apache.commons.io.IOUtils; import org.apache.james.blob.api.BlobId; import org.apache.james.blob.api.BlobStore; import org.apache.james.blob.api.BlobStoreContract; import org.apache.james.blob.api.HashBlobId; import org.apache.james.blob.api.ObjectStoreException; +import org.apache.james.blob.objectstorage.crypto.CryptoConfig; import org.apache.james.blob.objectstorage.swift.Credentials; import org.apache.james.blob.objectstorage.swift.Identity; import org.apache.james.blob.objectstorage.swift.PassHeaderName; @@ -47,11 +52,16 @@ public class ObjectStorageBlobsDAOTest implements BlobStoreContract { private static final UserName USER_NAME = UserName.of("tester"); private static final Credentials PASSWORD = Credentials.of("testing"); private static final Identity SWIFT_IDENTITY = Identity.of(TENANT_NAME, USER_NAME); + public static final String SAMPLE_SALT = "c603a7327ee3dcbc031d8d34b1096c605feca5e1"; private ContainerName containerName; private org.jclouds.blobstore.BlobStore blobStore; private SwiftTempAuthObjectStorage.Configuration testConfig; private ObjectStorageBlobsDAO testee; + public static final CryptoConfig CRYPTO_CONFIG = CryptoConfig.builder() + .salt(SAMPLE_SALT) + .password(PASSWORD.value().toCharArray()) + .build(); @BeforeEach void setUp(DockerSwift dockerSwift) throws Exception { @@ -64,12 +74,12 @@ public class ObjectStorageBlobsDAOTest implements BlobStoreContract { .tempAuthHeaderPassName(PassHeaderName.of("X-Storage-Pass")) .build(); BlobId.Factory blobIdFactory = blobIdFactory(); - blobStore = ObjectStorageBlobsDAO + ObjectStorageBlobsDAOBuilder daoBuilder = ObjectStorageBlobsDAO .builder(testConfig) .container(containerName) - .blobIdFactory(blobIdFactory) - .getSupplier().get(); - testee = new ObjectStorageBlobsDAO(containerName, blobIdFactory, blobStore); + .blobIdFactory(blobIdFactory); + blobStore = daoBuilder.getSupplier().get(); + testee = daoBuilder.build(); testee.createContainer(containerName); } @@ -104,5 +114,40 @@ public class ObjectStorageBlobsDAOTest implements BlobStoreContract { .hasCauseInstanceOf(ObjectStoreException.class) .hasMessageContaining("Unable to create container"); } + + @Test + void supportsEncryptionWithCustomPayloadCodec() throws Exception { + ObjectStorageBlobsDAO encryptedDao = ObjectStorageBlobsDAO + .builder(testConfig) + .container(containerName) + .blobIdFactory(blobIdFactory()) + .payloadCodec(new AESPayloadCodec(CRYPTO_CONFIG)) + .build(); + byte[] bytes = "James is the best!".getBytes(StandardCharsets.UTF_8); + BlobId blobId = encryptedDao.save(bytes).join(); + + InputStream read = encryptedDao.read(blobId); + assertThat(read).hasSameContentAs(new ByteArrayInputStream(bytes)); + } + + @Test + void encryptionWithCustomPayloadCodeCannotBeReadFromUnencryptedDAO() throws Exception { + ObjectStorageBlobsDAO encryptedDao = ObjectStorageBlobsDAO + .builder(testConfig) + .container(containerName) + .blobIdFactory(blobIdFactory()) + .payloadCodec(new AESPayloadCodec(CRYPTO_CONFIG)) + .build(); + byte[] bytes = "James is the best!".getBytes(StandardCharsets.UTF_8); + BlobId blobId = encryptedDao.save(bytes).join(); + + InputStream encryptedIs = testee.read(blobId); + assertThat(encryptedIs).isNotNull(); + byte[] encryptedBytes = IOUtils.toByteArray(encryptedIs); + assertThat(encryptedBytes).isNotEqualTo(bytes); + + InputStream clearTextIs = encryptedDao.read(blobId); + assertThat(clearTextIs).hasSameContentAs(new ByteArrayInputStream(bytes)); + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/PayloadCodecContract.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/PayloadCodecContract.java b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/PayloadCodecContract.java new file mode 100644 index 0000000..43dbf2a --- /dev/null +++ b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/PayloadCodecContract.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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +public interface PayloadCodecContract { + byte[] SOME_BYTES = "james".getBytes(StandardCharsets.UTF_8); + + @Test + default void shouldBeAbleToReadFromWrittenPayload() throws Exception { + PayloadCodec codec = codec(); + InputStream actual = codec.read(codec.write(expected())); + assertThat(actual).hasSameContentAs(expected()); + } + + default ByteArrayInputStream expected() { + return new ByteArrayInputStream(SOME_BYTES); + } + + PayloadCodec codec(); + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/3d1981d3/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilderTest.java ---------------------------------------------------------------------- diff --git a/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilderTest.java b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilderTest.java new file mode 100644 index 0000000..6430583 --- /dev/null +++ b/server/blob/blob-objectstorage/src/test/java/org/apache/james/blob/objectstorage/crypto/CryptoConfigBuilderTest.java @@ -0,0 +1,82 @@ +/**************************************************************** + * 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.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.google.crypto.tink.subtle.Hex; + +class CryptoConfigBuilderTest { + + public static final char[] PASSWORD = "password".toCharArray(); + public static final String SALT = "0123456789abcdef"; + + @Test + void shouldNotBuildCryptoConfigIfSaltMissing() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.password(PASSWORD); + assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldNotBuildCryptoConfigIfSaltEmpty() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.password(PASSWORD); + builder.salt(""); + assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldNotBuildCryptoConfigIfSaltNotHex() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.password(PASSWORD); + builder.salt("ghijk"); + assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void shouldNotBuildCryptoConfigIfPasswordMissing() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.salt(SALT); + assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldNotBuildCryptoConfigIfPasswordEmpty() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.salt(SALT); + builder.password("".toCharArray()); + assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class); + } + + @Test + void shouldBuildCryptoConfig() { + CryptoConfigBuilder builder = new CryptoConfigBuilder(); + builder.salt(SALT); + builder.password(PASSWORD); + CryptoConfig actual = builder.build(); + assertThat(actual.password()).isEqualTo(PASSWORD); + assertThat(actual.salt()).isEqualTo(Hex.decode(SALT)); + } + +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
