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; + } +}