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]

Reply via email to