diff --git a/config/elasticinbox.yaml b/config/elasticinbox.yaml
index c459f62..be9d587 100644
--- a/config/elasticinbox.yaml
+++ b/config/elasticinbox.yaml
@@ -96,10 +96,13 @@ blobstore_write_profile: fs-local
# Compress objects written to the blob store (including database blobs)
blobstore_enable_compression: true
-# Encrypt objects written to the blob store. Blobs stored in database are
-# never encrypted.
-blobstore_enable_encryption: false
-#blobstore_default_encryption_key: mykey1
+# Encrypt raw emails written to local machine and/or remote blob storage
+local_blobstore_enable_encryption: false
+remote_blobstore_enable_encryption: false
+# Encrypt parsed message data like body, sender, addresses written to local db
+metastore_enable_encryption: false
+
+#default_encryption_key: mykey1
### Encryption settings
#encryption:
diff --git a/itests/src/test/resources/elasticinbox.yaml b/itests/src/test/resources/elasticinbox.yaml
index 6ef0e6a..843402f 100644
--- a/itests/src/test/resources/elasticinbox.yaml
+++ b/itests/src/test/resources/elasticinbox.yaml
@@ -82,10 +82,15 @@ blobstore_write_profile: itest
# Compress objects written to the blob store (including database blobs)
blobstore_enable_compression: true
-# Encrypt objects written to the blob store. Blobs stored in database are
-# never encrypted.
-blobstore_enable_encryption: true
-blobstore_default_encryption_key: testkey2
+
+# Encrypt raw emails written to local machine and/or remote blob storage
+local_blobstore_enable_encryption: true
+remote_blobstore_enable_encryption: true
+# Encrypt parsed message data like body, sender, addresses written to local db
+metastore_enable_encryption: true
+
+default_encryption_key: testkey2
+
### Encryption settings
encryption:
diff --git a/modules/config/src/main/java/com/elasticinbox/config/Config.java b/modules/config/src/main/java/com/elasticinbox/config/Config.java
index 2faa0c5..293bc5b 100644
--- a/modules/config/src/main/java/com/elasticinbox/config/Config.java
+++ b/modules/config/src/main/java/com/elasticinbox/config/Config.java
@@ -71,9 +71,14 @@ public class Config
public Boolean blobstore_enable_compression;
// Blob store encryption
- public Boolean blobstore_enable_encryption = false;
- public String blobstore_default_encryption_key = null;
+ public Boolean remote_blobstore_enable_encryption = false;
+ public Boolean local_blobstore_enable_encryption = false;
+ public String default_encryption_key = null;
// Encryption options
public EncryptionSettings encryption = new EncryptionSettings();
+
+ // Meta store encryption
+ // Currently uses the same key as the blob store
+ public Boolean metastore_enable_encryption = false;
}
diff --git a/modules/config/src/main/java/com/elasticinbox/config/Configurator.java b/modules/config/src/main/java/com/elasticinbox/config/Configurator.java
index f249bbc..8e1ca4c 100644
--- a/modules/config/src/main/java/com/elasticinbox/config/Configurator.java
+++ b/modules/config/src/main/java/com/elasticinbox/config/Configurator.java
@@ -138,10 +138,11 @@ static URI getStorageConfigURL() throws ConfigurationException
keyManager = new SymmetricKeyStorage(keystoreFile, conf.encryption.keystore_password);
// verify that default blobstore encryption key exists
- if (!keyManager.containsKey(conf.blobstore_default_encryption_key)) {
+ if (!keyManager.containsKey(conf.default_encryption_key)) {
throw new ConfigurationException("Default encryption key for BlobStore '"
- + conf.blobstore_default_encryption_key + "' not found");
+ + conf.default_encryption_key + "' not found");
}
+
} else {
// initialize empty key store
keyManager = new SymmetricKeyStorage();
@@ -268,20 +269,33 @@ public static Boolean isBlobStoreCompressionEnabled() {
return conf.blobstore_enable_compression;
}
- public static Boolean isBlobStoreEncryptionEnabled() {
- return conf.blobstore_enable_encryption;
+ public static Boolean isRemoteBlobStoreEncryptionEnabled() {
+ return conf.remote_blobstore_enable_encryption;
}
-
- public static String getBlobStoreDefaultEncryptionKeyAlias() {
- return conf.blobstore_default_encryption_key;
+
+ public static Boolean isLocalBlobStoreEncryptionEnabled() {
+ return conf.local_blobstore_enable_encryption;
+ }
+
+ public static String getDefaultEncryptionKeyAlias() {
+ return conf.default_encryption_key;
}
public static java.security.Key getEncryptionKey(String alias) {
return keyManager.getKey(alias);
}
- public static java.security.Key getBlobStoreDefaultEncryptionKey() {
- return keyManager.getKey(conf.blobstore_default_encryption_key);
+ public static java.security.Key getDefaultEncryptionKey() {
+ return keyManager.getKey(conf.default_encryption_key);
+ }
+
+ public static boolean isMetaStoreEncryptionEnabled() {
+ return conf.metastore_enable_encryption;
+ }
+
+
+ public static java.security.Key getMetaStoreDefaultEncryptionKey() {
+ return keyManager.getKey(conf.default_encryption_key);
}
}
diff --git a/modules/core/pom.xml b/modules/core/pom.xml
index ac52bfe..936eae3 100644
--- a/modules/core/pom.xml
+++ b/modules/core/pom.xml
@@ -392,7 +392,6 @@
commons-codec
commons-codec
1.8
- test
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/encryption/AESEncryptionHandler.java b/modules/core/src/main/java/com/elasticinbox/core/blob/encryption/AESEncryptionHandler.java
deleted file mode 100644
index 0e5212f..0000000
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/encryption/AESEncryptionHandler.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright (c) 2011-2012 Optimax Software Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * * Neither the name of Optimax Software, ElasticInbox, nor the names
- * of its contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.elasticinbox.core.blob.encryption;
-
-import java.io.InputStream;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-
-/**
- * This class provides AES encryption/decryption methods.
- *
- * @author Rustam Aliyev
- */
-public class AESEncryptionHandler implements EncryptionHandler
-{
- /**
- * Use AES-CBC algorithm with PKCS5 padding
- */
- public static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
-
- public InputStream encrypt(InputStream in, Key key, byte[] iv)
- throws NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidKeyException, InvalidAlgorithmParameterException, NoSuchProviderException
- {
- Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
-
- return new CipherInputStream(in, cipher);
- }
-
- public InputStream decrypt(InputStream in, Key key, byte[] iv)
- throws NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidKeyException, InvalidAlgorithmParameterException, NoSuchProviderException
- {
- Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
-
- return new CipherInputStream(in, cipher);
- }
-}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorage.java b/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorage.java
index c1429c2..56be957 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorage.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorage.java
@@ -32,14 +32,19 @@
import java.io.InputStream;
import java.net.URI;
import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
import java.util.UUID;
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.BlobURI;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.model.Mailbox;
-public interface BlobStorage
-{
+public abstract class BlobStorage {
+
+
+ protected EncryptionHandler encryptionHandler;
+
/**
* Store blob contents, optionally compress and encrypt.
*
@@ -57,17 +62,19 @@ public interface BlobStorage
* @throws IOException
* @throws GeneralSecurityException
*/
- public BlobURI write(final UUID messageId, final Mailbox mailbox, final String profileName, final InputStream in, final Long size)
+ public abstract BlobURI write(final UUID messageId, final Mailbox mailbox,
+ final String profileName, final InputStream in, final Long size)
throws IOException, GeneralSecurityException;
/**
* Read blob contents and decrypt
*
- * @param uri Blob URI
+ * @param uri
+ * Blob URI
* @return
- * @throws IOException
+ * @throws IOException
*/
- public BlobDataSource read(final URI uri) throws IOException;
+ public abstract BlobDataSource read(final URI uri) throws IOException;
/**
* Delete blob
@@ -75,6 +82,6 @@ public BlobURI write(final UUID messageId, final Mailbox mailbox, final String p
* @param uri
* @throws IOException
*/
- public void delete(final URI uri) throws IOException;
+ public abstract void delete(final URI uri) throws IOException;
}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorageMediator.java b/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorageMediator.java
index 2673442..60af352 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorageMediator.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/blob/store/BlobStorageMediator.java
@@ -46,7 +46,8 @@
import com.elasticinbox.core.blob.BlobURI;
import com.elasticinbox.core.blob.compression.CompressionHandler;
import com.elasticinbox.core.blob.compression.DeflateCompressionHandler;
-import com.elasticinbox.core.blob.encryption.EncryptionHandler;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.model.Mailbox;
import com.google.common.io.ByteStreams;
import com.google.common.io.FileBackedOutputStream;
@@ -57,10 +58,14 @@
*
* @author Rustam Aliyev
*/
-public final class BlobStorageMediator implements BlobStorage
-{
- private static final Logger logger =
- LoggerFactory.getLogger(BlobStorageMediator.class);
+public final class BlobStorageMediator extends BlobStorage {
+ private static final Logger logger = LoggerFactory
+ .getLogger(BlobStorageMediator.class);
+
+ protected static byte[] getCipherIVFromBlobName(final String blobName)
+ throws IOException {
+ return null;
+ }
protected final CompressionHandler compressionHandler;
@@ -76,11 +81,22 @@ public final class BlobStorageMediator implements BlobStorage
* @param eh
* Injected encryption handler
*/
+
public BlobStorageMediator(final CompressionHandler ch, final EncryptionHandler eh)
{
this.compressionHandler = ch;
- cloudBlobStorage = new CloudBlobStorage(eh);
- dbBlobStorage = new CassandraBlobStorage();
+
+ if (Configurator.isRemoteBlobStoreEncryptionEnabled()) {
+ cloudBlobStorage = new CloudBlobStorage(eh);
+ } else {
+ cloudBlobStorage = new CloudBlobStorage(null);
+ }
+
+ if (Configurator.isLocalBlobStoreEncryptionEnabled()) {
+ dbBlobStorage = new CassandraBlobStorage(eh);
+ } else {
+ dbBlobStorage = new CassandraBlobStorage(null);
+ }
}
public BlobURI write(final UUID messageId, final Mailbox mailbox, final String profileName,
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/store/CassandraBlobStorage.java b/modules/core/src/main/java/com/elasticinbox/core/blob/store/CassandraBlobStorage.java
index c2116d1..850e644 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/store/CassandraBlobStorage.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/blob/store/CassandraBlobStorage.java
@@ -41,21 +41,26 @@
import org.slf4j.LoggerFactory;
import com.elasticinbox.common.utils.Assert;
+import com.elasticinbox.config.Configurator;
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.BlobURI;
+import com.elasticinbox.core.blob.BlobUtils;
+import com.elasticinbox.core.blob.naming.BlobNameBuilder;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.cassandra.persistence.BlobPersistence;
import com.elasticinbox.core.model.Mailbox;
import com.google.common.io.ByteStreams;
+import com.google.common.io.FileBackedOutputStream;
/**
* Blob storage proxy for Cassandra
*
* @author Rustam Aliyev
*/
-public final class CassandraBlobStorage implements BlobStorage
-{
- private static final Logger logger =
- LoggerFactory.getLogger(CassandraBlobStorage.class);
+public final class CassandraBlobStorage extends BlobStorage {
+ private static final Logger logger = LoggerFactory
+ .getLogger(CassandraBlobStorage.class);
/**
* Constructor
@@ -63,7 +68,17 @@ public final class CassandraBlobStorage implements BlobStorage
* Cassandra blob storage does not perform compression of encryption.
*/
public CassandraBlobStorage() {
-
+
+ }
+
+ /**
+ * Constructor
+ *
+ * @param eh
+ * Injected Encryption Handler
+ */
+ public CassandraBlobStorage(EncryptionHandler eh) {
+ encryptionHandler = eh;
}
@Override
@@ -74,12 +89,35 @@ public BlobURI write(final UUID messageId, final Mailbox mailbox, final String p
+ " bytes can't be stored in Cassandra. Provided blob size: " + size + " bytes");
logger.debug("Storing blob {} in Cassandra", messageId);
+ // get blob name
+ String blobName = new BlobNameBuilder().setMailbox(mailbox)
+ .setMessageId(messageId).setMessageSize(size).build();
// prepare URI
BlobURI blobUri = new BlobURI()
.setProfile(DATABASE_PROFILE)
.setName(messageId.toString()).setBlockCount(1);
+ InputStream in1;
+ // encrypt stream
+ if (encryptionHandler != null) {
+ byte[] iv = AESEncryptionHandler.getCipherIVFromBlobName(blobName);
+
+ InputStream encryptedInputStream = this.encryptionHandler.encrypt(
+ in, Configurator.getDefaultEncryptionKey(), iv);
+ FileBackedOutputStream fbout = new FileBackedOutputStream(
+ MAX_MEMORY_FILE_SIZE, true);
+
+ ByteStreams.copy(encryptedInputStream, fbout);
+
+ in1 = fbout.getSupplier().getInput();
+
+ blobUri.setEncryptionKey(Configurator
+ .getDefaultEncryptionKeyAlias());
+ } else {
+ in1 = in;
+ }
+
// store blob
// TODO: currently we allow only single block writes (blockid=0). in future we can split blobs to multiple blocks
BlobPersistence.writeBlock(messageId, DATABASE_DEFAULT_BLOCK_ID, ByteStreams.toByteArray(in));
@@ -98,6 +136,25 @@ public BlobDataSource read(final URI uri) throws IOException
UUID messageId = UUID.fromString(blobUri.getName());
byte[] messageBlock = BlobPersistence.readBlock(messageId, DATABASE_DEFAULT_BLOCK_ID);
InputStream in = ByteStreams.newInputStreamSupplier(messageBlock).getInput();
+ String keyAlias = blobUri.getEncryptionKey();
+
+ if (keyAlias != null) {
+ // currently we only support AES encryption, use by default
+ EncryptionHandler eh = new AESEncryptionHandler();
+
+ try {
+ logger.debug("Decrypting object {} with key {}", uri, keyAlias);
+
+ byte[] iv = AESEncryptionHandler.getCipherIVFromBlobName(BlobUtils.relativize(uri
+ .getPath()));
+
+ in = eh.decrypt(in, Configurator.getEncryptionKey(keyAlias), iv);
+
+ // Configurator.getEncryptionKey(keyAlias), iv);
+ } catch (GeneralSecurityException gse) {
+ throw new IOException("Unable to decrypt message blob: ", gse);
+ }
+ }
return new BlobDataSource(uri, in);
}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/store/CloudBlobStorage.java b/modules/core/src/main/java/com/elasticinbox/core/blob/store/CloudBlobStorage.java
index eff1cc9..cfd9754 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/store/CloudBlobStorage.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/blob/store/CloudBlobStorage.java
@@ -34,7 +34,6 @@
import java.io.InputStream;
import java.net.URI;
import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
import java.util.UUID;
import org.slf4j.Logger;
@@ -44,15 +43,14 @@
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.BlobURI;
import com.elasticinbox.core.blob.BlobUtils;
-import com.elasticinbox.core.blob.encryption.AESEncryptionHandler;
-import com.elasticinbox.core.blob.encryption.EncryptionHandler;
import com.elasticinbox.core.blob.naming.BlobNameBuilder;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.model.Mailbox;
import com.google.common.io.ByteStreams;
import com.google.common.io.FileBackedOutputStream;
-public final class CloudBlobStorage implements BlobStorage
-{
+public final class CloudBlobStorage extends BlobStorage {
private static final Logger logger =
LoggerFactory.getLogger(CloudBlobStorage.class);
@@ -84,17 +82,19 @@ public BlobURI write(final UUID messageId, final Mailbox mailbox, final String p
.setName(blobName);
// encrypt stream
- if (encryptionHandler != null)
- {
- byte[] iv = getCipherIVFromBlobName(blobName);
-
- InputStream encryptedInputStream = this.encryptionHandler.encrypt(in, Configurator.getBlobStoreDefaultEncryptionKey(), iv);
- FileBackedOutputStream fbout = new FileBackedOutputStream(MAX_MEMORY_FILE_SIZE, true);
-
+ if (encryptionHandler != null) {
+ byte[] iv = AESEncryptionHandler.getCipherIVFromBlobName(blobName);
+
+ InputStream encryptedInputStream = this.encryptionHandler.encrypt(
+ in, Configurator.getDefaultEncryptionKey(), iv);
+ FileBackedOutputStream fbout = new FileBackedOutputStream(
+ MAX_MEMORY_FILE_SIZE, true);
+
updatedSize = ByteStreams.copy(encryptedInputStream, fbout);
in1 = fbout.getSupplier().getInput();
- blobUri.setEncryptionKey(Configurator.getBlobStoreDefaultEncryptionKeyAlias());
+ blobUri.setEncryptionKey(Configurator
+ .getDefaultEncryptionKeyAlias());
} else {
in1 = in;
}
@@ -115,14 +115,15 @@ public BlobDataSource read(final URI uri) throws IOException
if (keyAlias != null)
{
// currently we only support AES encryption, use by default
- EncryptionHandler eh = new AESEncryptionHandler();
try {
logger.debug("Decrypting object {} with key {}", uri, keyAlias);
- byte[] iv = getCipherIVFromBlobName(BlobUtils.relativize(uri.getPath()));
+ byte[] iv = AESEncryptionHandler
+ .getCipherIVFromBlobName(BlobUtils.relativize(uri
+ .getPath()));
- in = eh.decrypt(CloudStoreProxy.read(uri),
+ in = encryptionHandler.decrypt(CloudStoreProxy.read(uri),
Configurator.getEncryptionKey(keyAlias), iv);
} catch (GeneralSecurityException gse) {
throw new IOException("Unable to decrypt message blob: ", gse);
@@ -139,30 +140,5 @@ public void delete(final URI uri) throws IOException
{
CloudStoreProxy.delete(uri);
}
-
- /**
- * Generate cipher initialisation vector (IV) from Blob name.
- *
- * IV should be unique but not necessarily secure. Since blob names are
- * based on Type1 UUID they are unique.
- *
- * @param blobName
- * @return
- * @throws IOException
- */
- private static byte[] getCipherIVFromBlobName(final String blobName) throws IOException
- {
- byte[] iv;
-
- try {
- byte[] nameBytes = blobName.getBytes("UTF-8");
- MessageDigest md = MessageDigest.getInstance("MD5");
- iv = md.digest(nameBytes);
- } catch (Exception e) {
- // should never happen
- throw new IOException(e);
- }
- return iv;
- }
}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/cassandra/CassandraMessageDAO.java b/modules/core/src/main/java/com/elasticinbox/core/cassandra/CassandraMessageDAO.java
index eaf2e90..a919305 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/cassandra/CassandraMessageDAO.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/cassandra/CassandraMessageDAO.java
@@ -33,6 +33,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.security.Key;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
@@ -56,13 +57,19 @@
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.compression.CompressionHandler;
import com.elasticinbox.core.blob.compression.DeflateCompressionHandler;
-import com.elasticinbox.core.blob.encryption.AESEncryptionHandler;
-import com.elasticinbox.core.blob.encryption.EncryptionHandler;
+import com.elasticinbox.core.blob.naming.BlobNameBuilder;
import com.elasticinbox.core.blob.store.BlobStorage;
import com.elasticinbox.core.blob.store.BlobStorageMediator;
-import com.elasticinbox.core.cassandra.persistence.*;
+import com.elasticinbox.core.cassandra.persistence.AccountPersistence;
+import com.elasticinbox.core.cassandra.persistence.LabelCounterPersistence;
+import com.elasticinbox.core.cassandra.persistence.LabelIndexPersistence;
+import com.elasticinbox.core.cassandra.persistence.Marshaller;
+import com.elasticinbox.core.cassandra.persistence.MessagePersistence;
+import com.elasticinbox.core.cassandra.persistence.PurgeIndexPersistence;
import com.elasticinbox.core.cassandra.utils.BatchConstants;
import com.elasticinbox.core.cassandra.utils.ThrottlingMutator;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.model.LabelCounters;
import com.elasticinbox.core.model.Labels;
import com.elasticinbox.core.model.Mailbox;
@@ -79,24 +86,65 @@ public final class CassandraMessageDAO extends AbstractMessageDAO implements Mes
private final static Logger logger =
LoggerFactory.getLogger(CassandraMessageDAO.class);
-
- public CassandraMessageDAO(Keyspace keyspace)
- {
+
+ private EncryptionHandler encryptionHandler;
+
+ public CassandraMessageDAO(Keyspace keyspace) {
this.keyspace = keyspace;
-
- // Create BlobStorage instance with AES encryption and Deflate compression
- CompressionHandler compressionHandler =
- Configurator.isBlobStoreCompressionEnabled() ? new DeflateCompressionHandler() : null;
- EncryptionHandler encryptionHandler =
- Configurator.isBlobStoreEncryptionEnabled() ? new AESEncryptionHandler() : null;
- this.blobStorage = new BlobStorageMediator(compressionHandler, encryptionHandler);
+ // Create BlobStorage instance with optional AES encryption and
+ // Deflate compression
+ CompressionHandler compressionHandler = Configurator
+ .isBlobStoreCompressionEnabled() ? new DeflateCompressionHandler()
+ : null;
+
+
+ if (Configurator.isRemoteBlobStoreEncryptionEnabled() || Configurator.isLocalBlobStoreEncryptionEnabled()) {
+ this.blobStorage = new BlobStorageMediator(compressionHandler,
+ new AESEncryptionHandler());
+ } else {
+ this.blobStorage = new BlobStorageMediator(compressionHandler, null);
+ }
+
+ // enable encryption of CassandraMessageDAO if enabled
+ encryptionHandler = Configurator.isMetaStoreEncryptionEnabled() ? new AESEncryptionHandler()
+ : null;
+ }
+
+ private Message decryptMessageIfNecessary(Mailbox mailbox, UUID messageId, Message message) {
+ String blobName = new BlobNameBuilder().setMailbox(mailbox)
+ .setMessageId(messageId).build();
+
+ if (encryptionHandler != null) {
+
+ try {
+ byte[] iv;
+ iv = AESEncryptionHandler.getCipherIVFromBlobName(blobName);
+ // decrypt message using the stored encryption key alias
+ String keyAlias = message.getEncryptionKey();
+
+ logger.debug("Decrypting object {} with key {}", messageId,
+ keyAlias);
+ Key key = Configurator.getEncryptionKey(keyAlias);
+ message = encryptionHandler.decryptMessage(message, key, iv);
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ logger.error("unable to decrypt message: key={}",
+ message.getMessageId());
+ }
+ }
+ return message;
}
@Override
- public Message getParsed(final Mailbox mailbox, final UUID messageId)
- {
- return MessagePersistence.fetch(mailbox.getId(), messageId, true);
+ public Message getParsed(final Mailbox mailbox, final UUID messageId) {
+ Message message = MessagePersistence.fetch(mailbox.getId(), messageId,
+ true);
+
+ // decrypt message metadata if a handler is set
+ message = decryptMessageIfNecessary(mailbox, messageId, message);
+
+ return message;
}
@Override
@@ -109,12 +157,18 @@ public BlobDataSource getRaw(final Mailbox mailbox, final UUID messageId)
@Override
public Map getMessageIdsWithHeaders(final Mailbox mailbox,
- final int labelId, final UUID start, final int count, boolean reverse)
- {
- List messageIds =
- getMessageIds(mailbox, labelId, start, count, reverse);
+ final int labelId, final UUID start, final int count,
+ boolean reverse) {
+ List messageIds = getMessageIds(mailbox, labelId, start, count,
+ reverse);
+
+ Map messages = MessagePersistence.fetch(mailbox.getId(), messageIds, false);
+ for (Map.Entry message : messages.entrySet())
+ {
+ message.setValue(decryptMessageIfNecessary(mailbox, message.getKey(), message.getValue()));
+ }
- return MessagePersistence.fetch(mailbox.getId(), messageIds, false);
+ return messages;
}
@Override
@@ -125,9 +179,8 @@ public List getMessageIds(final Mailbox mailbox, final int labelId,
}
@Override
- public void put(final Mailbox mailbox, UUID messageId, Message message, InputStream in)
- throws IOException, OverQuotaException
- {
+ public void put(final Mailbox mailbox, UUID messageId, Message message,
+ InputStream in) throws IOException, OverQuotaException {
URI uri = null;
logger.debug("Storing message: key={}", messageId.toString());
@@ -135,13 +188,14 @@ public void put(final Mailbox mailbox, UUID messageId, Message message, InputStr
LabelCounters mailboxCounters = LabelCounterPersistence.get(
mailbox.getId(), ReservedLabels.ALL_MAILS.getId());
- long requiredBytes = mailboxCounters.getTotalBytes() + message.getSize();
+ long requiredBytes = mailboxCounters.getTotalBytes()
+ + message.getSize();
long requiredCount = mailboxCounters.getTotalMessages() + 1;
- if ((requiredBytes > Configurator.getDefaultQuotaBytes()) ||
- (requiredCount > Configurator.getDefaultQuotaCount()))
- {
- logger.info("Mailbox is over quota: {} size={}/{}, count={}/{}",
+ if ((requiredBytes > Configurator.getDefaultQuotaBytes())
+ || (requiredCount > Configurator.getDefaultQuotaCount())) {
+ logger.info(
+ "Mailbox is over quota: {} size={}/{}, count={}/{}",
new Object[] { mailbox.getId(), requiredBytes,
Configurator.getDefaultQuotaBytes(), requiredCount,
Configurator.getDefaultQuotaCount() });
@@ -152,12 +206,11 @@ public void put(final Mailbox mailbox, UUID messageId, Message message, InputStr
// Order is important, add to label after message written
// store blob
- if (in != null)
- {
+ if (in != null) {
try {
uri = blobStorage.write(messageId, mailbox,
- Configurator.getBlobStoreWriteProfileName(), in, message.getSize())
- .buildURI();
+ Configurator.getBlobStoreWriteProfileName(), in,
+ message.getSize()).buildURI();
// update location in metadata
message.setLocation(uri);
@@ -177,6 +230,21 @@ public void put(final Mailbox mailbox, UUID messageId, Message message, InputStr
// begin batch operation
Mutator m = createMutator(keyspace, strSe);
+ // encrypt message metadata if a handler is set
+ if (encryptionHandler != null) {
+ String blobName = new BlobNameBuilder().setMailbox(mailbox)
+ .setMessageId(messageId).build();
+
+ // encrypt message
+ byte[] iv = AESEncryptionHandler
+ .getCipherIVFromBlobName(blobName);
+
+ message = encryptionHandler.encryptMessage(message,
+ Configurator.getDefaultEncryptionKey(), iv);
+
+ message.setEncryptionKey(Configurator
+ .getDefaultEncryptionKeyAlias());
+ }
// store metadata
MessagePersistence.persistMessage(m, mailbox.getId(), messageId, message);
// add indexes
diff --git a/modules/core/src/main/java/com/elasticinbox/core/cassandra/persistence/Marshaller.java b/modules/core/src/main/java/com/elasticinbox/core/cassandra/persistence/Marshaller.java
index 15bf0c3..9153e67 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/cassandra/persistence/Marshaller.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/cassandra/persistence/Marshaller.java
@@ -68,6 +68,8 @@ public final class Marshaller
public final static String CN_BRI = "bri"; // Blob Resource Identifier
public final static String CN_LABEL_PREFIX = "l:";
public final static String CN_MARKER_PREFIX = "m:";
+ public final static String CN_KEY = "key";
+
private final static DateSerializer dateSe = DateSerializer.get();
private final static LongSerializer longSe = LongSerializer.get();
@@ -139,6 +141,8 @@ protected static Message unmarshall(
Map parts = null;
parts = JSONUtils.toObject(c.getValue(), parts);
message.setParts(parts);
+ } else if (c.getName().equals(CN_KEY)) {
+ message.setEncryptionKey(strSe.fromBytes(c.getValue()));
}
}
}
@@ -228,6 +232,11 @@ protected static List> marshall(final Message m)
if (Configurator.isStorePlainWithMetadata() && (m.getPlainBody() != null)) {
columns.put(CN_PLAIN_BODY, IOUtils.compress(m.getPlainBody()));
}
+
+ // add encryption alias
+ if (m.getEncryptionKey() != null) {
+ columns.put(CN_KEY, m.getEncryptionKey());
+ }
return mapToHColumns(columns);
}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/encryption/AESEncryptionHandler.java b/modules/core/src/main/java/com/elasticinbox/core/encryption/AESEncryptionHandler.java
new file mode 100644
index 0000000..520cbeb
--- /dev/null
+++ b/modules/core/src/main/java/com/elasticinbox/core/encryption/AESEncryptionHandler.java
@@ -0,0 +1,255 @@
+/**
+ * Copyright (c) 2011-2012 Optimax Software Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Optimax Software, ElasticInbox, nor the names
+ * of its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.elasticinbox.core.encryption;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Iterator;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.elasticinbox.core.model.Address;
+import com.elasticinbox.core.model.AddressList;
+import com.elasticinbox.core.model.Message;
+
+/**
+ * This class provides AES encryption/decryption methods.
+ *
+ * @author
+ * - Rustam Aliyev
+ * - itembase GmbH, John Wiesel
+ *
+ */
+public class AESEncryptionHandler implements EncryptionHandler {
+
+ private static final Logger logger = LoggerFactory
+ .getLogger(AESEncryptionHandler.class);
+
+ /**
+ * Use AES-CBC algorithm with PKCS5 padding
+ */
+ public static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+
+ private Cipher cipher;
+
+ private static final int ENCRYPT = 1;
+ private static final int DECRYPT = -1;
+
+ private int mode = ENCRYPT;
+
+ public InputStream encrypt(InputStream in, Key key, byte[] iv)
+ throws NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, InvalidAlgorithmParameterException,
+ NoSuchProviderException {
+
+ cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ this.mode = ENCRYPT;
+
+ return new CipherInputStream(in, cipher);
+ }
+
+ public InputStream decrypt(InputStream in, Key key, byte[] iv)
+ throws NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, InvalidAlgorithmParameterException,
+ NoSuchProviderException {
+
+ cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+ this.mode = DECRYPT;
+
+ return new CipherInputStream(in, cipher);
+ }
+
+ /*
+ * encrypts most message object attributes, in particular: AddressLists
+ * from, to, cc, bcc and Strings subject plainBody htmlBody
+ */
+ public Message encryptMessage(Message message, Key key, byte[] iv)
+ throws NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, InvalidAlgorithmParameterException {
+
+ cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ this.mode = ENCRYPT;
+
+ return cryptMessage(message, key, iv);
+ }
+
+ /*
+ * decrypt a message object
+ */
+ public Message decryptMessage(Message message, Key key, byte[] iv)
+ throws NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, InvalidAlgorithmParameterException {
+
+ cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ this.cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+ this.mode = DECRYPT;
+
+ return cryptMessage(message, key, iv);
+ }
+
+ private Message cryptMessage(Message message, Key key, byte[] iv) {
+
+ if (message.getPlainBody() != null) {
+ message.setPlainBody(cryptString(message.getPlainBody(), key, iv));
+ }
+ if (message.getHtmlBody() != null) {
+ message.setHtmlBody(cryptString(message.getHtmlBody(), key, iv));
+ }
+ if (message.getSubject() != null) {
+ message.setSubject(cryptString(message.getSubject(), key, iv));
+ }
+ if (message.getFrom() != null) {
+ if (!message.getFrom().isEmpty()) {
+ message.setFrom(cryptAddressList(message.getFrom(), key, iv));
+ }
+ }
+ if (message.getTo() != null) {
+ if (!message.getTo().isEmpty()) {
+ message.setTo(cryptAddressList(message.getTo(), key, iv));
+ }
+ }
+
+ if (message.getCc() != null) {
+ if (!message.getCc().isEmpty()) {
+ message.setCc(cryptAddressList(message.getCc(), key, iv));
+ }
+ }
+ if (message.getBcc() != null) {
+ if (!message.getBcc().isEmpty()) {
+ message.setBcc(cryptAddressList(message.getBcc(), key, iv));
+ }
+ }
+ return message;
+ }
+
+ private AddressList cryptAddressList(AddressList from, Key key, byte[] iv) {
+
+ AddressList temp = new AddressList();
+ Iterator addresses = from.iterator();
+
+ if (addresses.hasNext()) {
+ Address address = cryptAddress(addresses.next(), key, iv);
+ temp = new AddressList(address);
+ while (addresses.hasNext()) {
+ address = cryptAddress(addresses.next(), key, iv);
+ temp.add(address);
+ }
+ }
+
+ return temp;
+ }
+
+ private Address cryptAddress(Address address, Key key, byte[] iv) {
+
+ String name = cryptString(address.getName(), key, iv);
+ String addressString = cryptString(address.getAddress(), key, iv);
+ address = new Address(name, addressString);
+ return address;
+ }
+
+ private String cryptString(String toCrypt, Key key, byte[] iv) {
+ if (this.mode == ENCRYPT) {
+ return symmetricEncrypt(toCrypt, key, iv);
+ }
+ return symmetricDecrypt(toCrypt, key, iv);
+ }
+
+ private String symmetricEncrypt(String text, Key secretKey, byte[] iv) {
+ String encryptedString = "";
+ byte[] encryptText = text.getBytes();
+
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ encryptedString = Base64.encodeBase64String(cipher
+ .doFinal(encryptText));
+
+ } catch (Exception e) {
+ logger.error("Error during symmetric encryption", e);
+ }
+
+ return encryptedString;
+ }
+
+ public String symmetricDecrypt(String text, Key secretKey, byte[] iv) {
+ String encryptedString = "";
+ byte[] encryptText = null;
+
+ try {
+ encryptText = Base64.decodeBase64(text);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ encryptedString = new String(cipher.doFinal(encryptText));
+
+ } catch (Exception e) {
+ logger.error("Error during symmetric decryption", e);
+ }
+ return encryptedString;
+ }
+
+ /**
+ * Generate cipher initialisation vector (IV) from Blob name.
+ *
+ * IV should be unique but not necessarily secure. Since blob names are
+ * based on Type1 UUID they are unique.
+ *
+ * @param blobName
+ * @return
+ * @throws IOException
+ */
+ public static byte[] getCipherIVFromBlobName(final String blobName)
+ throws IOException {
+ byte[] iv;
+
+ try {
+ byte[] nameBytes = blobName.getBytes("UTF-8");
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ iv = md.digest(nameBytes);
+ } catch (Exception e) {
+ // should never happen
+ throw new IOException(e);
+ }
+
+ return iv;
+ }
+}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/blob/encryption/EncryptionHandler.java b/modules/core/src/main/java/com/elasticinbox/core/encryption/EncryptionHandler.java
similarity index 70%
rename from modules/core/src/main/java/com/elasticinbox/core/blob/encryption/EncryptionHandler.java
rename to modules/core/src/main/java/com/elasticinbox/core/encryption/EncryptionHandler.java
index 2a1238c..c292b31 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/blob/encryption/EncryptionHandler.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/encryption/EncryptionHandler.java
@@ -26,11 +26,21 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package com.elasticinbox.core.blob.encryption;
+package com.elasticinbox.core.encryption;
import java.io.InputStream;
import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.ShortBufferException;
+
+import com.elasticinbox.core.model.Message;
/**
* Handles generic encryption/decryption tasks. Used mainly for dependency injection.
@@ -60,4 +70,16 @@ public interface EncryptionHandler
* @throws GeneralSecurityException
*/
public InputStream decrypt(InputStream in, Key key, byte[] iv) throws GeneralSecurityException;
+
+ /*
+ * encrypt a message object
+ */
+ public Message encryptMessage(Message message,
+ Key blobStoreDefaultEncryptionKey, byte[] iv) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, ShortBufferException, BadPaddingException, IllegalBlockSizeException, BadPaddingException;
+
+ /*
+ * decrypt a message object
+ */
+ public Message decryptMessage(Message message,
+ Key blobStoreDefaultEncryptionKey, byte[] iv) throws NoSuchAlgorithmException, NoSuchPaddingException, ShortBufferException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException;
}
diff --git a/modules/core/src/main/java/com/elasticinbox/core/model/AddressList.java b/modules/core/src/main/java/com/elasticinbox/core/model/AddressList.java
index 5ef1633..132416d 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/model/AddressList.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/model/AddressList.java
@@ -43,6 +43,14 @@ public class AddressList extends AbstractList
{
private final List addresses;
+ /**
+ * Create new empty address list
+ *
+ */
+ public AddressList() {
+ this.addresses = Collections.emptyList();
+ }
+
/**
* Create new address list
*
diff --git a/modules/core/src/main/java/com/elasticinbox/core/model/Message.java b/modules/core/src/main/java/com/elasticinbox/core/model/Message.java
index 78a65f8..ce5cbc8 100644
--- a/modules/core/src/main/java/com/elasticinbox/core/model/Message.java
+++ b/modules/core/src/main/java/com/elasticinbox/core/model/Message.java
@@ -267,6 +267,15 @@ public boolean isEncrypted() {
return this.encryptionKey != null;
}
+ public void setEncryptionKey(String key) {
+ this.encryptionKey = key;
+ }
+
+ public String getEncryptionKey(){
+ return this.encryptionKey;
+ }
+
+
/**
* Get message header by name
*
diff --git a/modules/core/src/test/java/com/elasticinbox/core/blob/store/CassandraStorageTest.java b/modules/core/src/test/java/com/elasticinbox/core/blob/store/CassandraStorageTest.java
index 677a270..68df20f 100644
--- a/modules/core/src/test/java/com/elasticinbox/core/blob/store/CassandraStorageTest.java
+++ b/modules/core/src/test/java/com/elasticinbox/core/blob/store/CassandraStorageTest.java
@@ -47,6 +47,8 @@
import com.elasticinbox.config.Configurator;
import com.elasticinbox.config.DatabaseConstants;
import com.elasticinbox.core.blob.BlobDataSource;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.EncryptionHandler;
import com.elasticinbox.core.model.Mailbox;
public class CassandraStorageTest
@@ -106,6 +108,41 @@ public void testLargeBlobStorage() throws IOException, GeneralSecurityException
testWrite(bs, TEST_LARGE_FILE);
}
+ /*
+ * @author itembase GmbH, John Wiesel
+ */
+ @Test
+ public void testEncryptedBlobStorage() throws IOException, GeneralSecurityException
+ {
+ String expextedBlobUrl = "blob://"
+ + DatabaseConstants.DATABASE_PROFILE + "/"
+ + MESSAGE_ID + "?"
+ + BlobStoreConstants.URI_PARAM_ENCRYPTION_KEY + "=" + Configurator.getDefaultEncryptionKeyAlias() + "&"
+ + BlobStoreConstants.URI_PARAM_BLOCK_COUNT + "=1";
+
+ EncryptionHandler encryptionHandler = new AESEncryptionHandler();
+
+ // BlobStorage with encryption
+ BlobStorage bs = new CassandraBlobStorage(encryptionHandler);
+
+ // Write blob
+ long origSize = testWrite(bs, TEST_FILE);
+
+ // Check written Blob URI
+ assertThat(blobUri.toString(), equalTo(expextedBlobUrl));
+
+ // Read blob back
+ BlobDataSource ds = bs.read(blobUri);
+
+ long newSize = IOUtils.getInputStreamSize(ds.getUncompressedInputStream());
+
+ // Check written Blob size
+ assertThat(newSize, equalTo(origSize));
+
+ // Delete
+ bs.delete(blobUri);
+ }
+
private long testWrite(BlobStorage bs, String filename) throws IOException, GeneralSecurityException
{
File file = new File(filename);
diff --git a/modules/core/src/test/java/com/elasticinbox/core/blob/store/CloudStorageTest.java b/modules/core/src/test/java/com/elasticinbox/core/blob/store/CloudStorageTest.java
index 406e31b..634ec52 100644
--- a/modules/core/src/test/java/com/elasticinbox/core/blob/store/CloudStorageTest.java
+++ b/modules/core/src/test/java/com/elasticinbox/core/blob/store/CloudStorageTest.java
@@ -49,7 +49,7 @@
import com.elasticinbox.config.Configurator;
import com.elasticinbox.core.blob.BlobDataSource;
import com.elasticinbox.core.blob.compression.DeflateCompressionHandler;
-import com.elasticinbox.core.blob.encryption.AESEncryptionHandler;
+import com.elasticinbox.core.encryption.AESEncryptionHandler;
import com.elasticinbox.core.model.Mailbox;
public class CloudStorageTest
@@ -108,7 +108,7 @@ public void testBlobStorageWithEcnryption() throws IOException, GeneralSecurityE
// + BlobStoreConstants.URI_PARAM_COMPRESSION + "="
// + DeflateCompressionHandler.COMPRESSION_TYPE_DEFLATE + "&"
+ BlobStoreConstants.URI_PARAM_ENCRYPTION_KEY + "="
- + Configurator.getBlobStoreDefaultEncryptionKeyAlias();
+ + Configurator.getDefaultEncryptionKeyAlias();
// BlobStorage with encryption or compression
BlobStorage bs = new CloudBlobStorage(new AESEncryptionHandler());
diff --git a/modules/core/src/test/java/com/elasticinbox/core/cassandra/CassandraEncryptedMessageDAOTest.java b/modules/core/src/test/java/com/elasticinbox/core/cassandra/CassandraEncryptedMessageDAOTest.java
new file mode 100644
index 0000000..2f5f998
--- /dev/null
+++ b/modules/core/src/test/java/com/elasticinbox/core/cassandra/CassandraEncryptedMessageDAOTest.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2011-2012 Optimax Software Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Optimax Software, ElasticInbox, nor the names
+ * of its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.elasticinbox.core.cassandra;
+
+import static me.prettyprint.hector.api.factory.HFactory.createMutator;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
+import static org.hamcrest.CoreMatchers.*;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.UUID;
+
+import me.prettyprint.cassandra.serializers.BytesArraySerializer;
+import me.prettyprint.cassandra.serializers.StringSerializer;
+import me.prettyprint.cassandra.serializers.UUIDSerializer;
+import me.prettyprint.cassandra.service.CassandraHostConfigurator;
+import me.prettyprint.hector.api.Cluster;
+import me.prettyprint.hector.api.ConsistencyLevelPolicy;
+import me.prettyprint.hector.api.Keyspace;
+import me.prettyprint.hector.api.factory.HFactory;
+import me.prettyprint.hector.api.mutation.Mutator;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.elasticinbox.config.Configurator;
+import com.elasticinbox.core.AccountDAO;
+import com.elasticinbox.core.MessageDAO;
+import com.elasticinbox.core.OverQuotaException;
+import com.elasticinbox.core.cassandra.utils.QuorumConsistencyLevel;
+import com.elasticinbox.core.message.id.MessageIdBuilder;
+import com.elasticinbox.core.model.Address;
+import com.elasticinbox.core.model.AddressList;
+import com.elasticinbox.core.model.Mailbox;
+import com.elasticinbox.core.model.Message;
+import com.elasticinbox.core.model.ReservedLabels;
+
+/*
+ * @author itembase GmbH, John Wiesel
+ */
+public class CassandraEncryptedMessageDAOTest {
+ final static StringSerializer strSe = StringSerializer.get();
+ final static UUIDSerializer uuidSe = UUIDSerializer.get();
+ final static BytesArraySerializer byteSe = BytesArraySerializer.get();
+
+ final static String KEYSPACE = "ElasticInbox";
+ final static String MAILBOX = "testmessagedao@elasticinbox.com";
+ Cluster cluster;
+ Keyspace keyspace;
+ CassandraDAOFactory dao;
+
+ @Before
+ public void setupCase() throws IllegalArgumentException, IOException {
+ System.setProperty("elasticinbox.config",
+ "../../config/elasticinbox.yaml");
+
+ // Consistency Level Policy
+ ConsistencyLevelPolicy clp = new QuorumConsistencyLevel();
+
+ // Host config
+ CassandraHostConfigurator conf = new CassandraHostConfigurator("127.0.0.1:9160");
+
+ cluster = HFactory.getOrCreateCluster("TestCluster", conf);
+ keyspace = HFactory.createKeyspace(KEYSPACE, cluster, clp);
+
+ dao = new CassandraDAOFactory();
+ CassandraDAOFactory.setKeyspace(keyspace);
+ AccountDAO accountDAO = dao.getAccountDAO();
+ accountDAO.add(new Mailbox(MAILBOX));
+ }
+
+ @After
+ public void teardownCase() throws IOException {
+ keyspace = null;
+ cluster = null;
+
+ AccountDAO accountDAO = dao.getAccountDAO();
+ accountDAO.delete(new Mailbox(MAILBOX));
+ }
+
+ @Test
+ public void testEncryptedMessageStorage() throws IOException,
+ OverQuotaException {
+
+ /*
+ * if meta storage encryption is not enabled, there is nothing to test
+ * here
+ */
+ Assume.assumeTrue(Configurator.isMetaStoreEncryptionEnabled());
+
+ Mailbox mailbox = new Mailbox(MAILBOX);
+
+ Message message = getDummyMessage();
+
+ MessageDAO messageDAO = dao.getMessageDAO();
+
+ UUID messageId = new MessageIdBuilder().build();
+ messageDAO.put(mailbox, messageId, message, null);
+
+ Mutator m = createMutator(keyspace, strSe);
+ m.execute();
+
+ Message readMessage = messageDAO.getParsed(mailbox, messageId);
+
+ /* compare original message and encrypted message */
+ assertThat(message.getPlainBody(),
+ not(getDummyMessage().getPlainBody()));
+ assertThat(message.getFrom().getDisplayString(), not(getDummyMessage()
+ .getFrom().getDisplayString()));
+ assertThat(message.getTo().getDisplayString(), not(getDummyMessage()
+ .getTo().getDisplayString()));
+ assertThat(message.getSubject(), not(getDummyMessage().getSubject()));
+ assertThat(getDummyMessage().getDate().compareTo(message.getDate()),
+ is(1));
+ assertThat(message.getPlainBody(),
+ not(getDummyMessage().getPlainBody()));
+ assertThat(message.getHtmlBody(), is(getDummyMessage().getHtmlBody()));
+
+ /* compare original message and decrypted message */
+ assertThat(readMessage.getFrom().getDisplayString(),
+ is(getDummyMessage().getFrom().getDisplayString()));
+ assertThat(readMessage.getTo().getDisplayString(), is(getDummyMessage()
+ .getTo().getDisplayString()));
+ assertThat(readMessage.getCc(), is(getDummyMessage().getCc()));
+ assertThat(readMessage.getBcc(), is(getDummyMessage().getBcc()));
+ assertThat(readMessage.getSubject(), is(getDummyMessage().getSubject()));
+ assertThat(
+ getDummyMessage().getDate().compareTo(readMessage.getDate()),
+ is(1));
+ assertThat(readMessage.getPlainBody(), is(getDummyMessage()
+ .getPlainBody()));
+ assertThat(getDummyMessage().getHtmlBody(),
+ is(readMessage.getHtmlBody()));
+
+ // delete all message ids
+ messageDAO.delete(mailbox, messageId);
+
+ }
+
+ private static Message getDummyMessage() {
+ Address address = new Address("Test", "test@elasticinbox.com");
+ AddressList al = new AddressList(address);
+
+ Message message = new Message();
+ message.setFrom(al);
+ message.setTo(al);
+ message.setSize(1024L);
+ message.setSubject("Test");
+ message.setPlainBody("Test");
+ message.setDate(new Date());
+ message.addLabel(ReservedLabels.ALL_MAILS);
+
+ return message;
+ }
+}