/** * 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.store; import static com.elasticinbox.core.blob.store.BlobStoreConstants.MAX_MEMORY_FILE_SIZE; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.encryption.AESEncryptionHandler; import com.elasticinbox.core.blob.encryption.EncryptionHandler; import com.elasticinbox.core.blob.naming.BlobNameBuilder; import com.elasticinbox.core.model.Mailbox; import com.google.common.io.ByteStreams; import com.google.common.io.FileBackedOutputStream; public final class CloudBlobStorage implements BlobStorage { private static final Logger logger = LoggerFactory.getLogger(CloudBlobStorage.class); private final EncryptionHandler encryptionHandler; /** * Constructor * * @param eh Injected Encryption Handler */ public CloudBlobStorage(EncryptionHandler eh) { this.encryptionHandler = eh; } @Override public BlobURI write(final UUID messageId, final Mailbox mailbox, final String profileName, final InputStream in, final Long size) throws IOException, GeneralSecurityException { // get blob name String blobName = new BlobNameBuilder().setMailbox(mailbox) .setMessageId(messageId).setMessageSize(size).build(); InputStream in1; Long updatedSize = size; // prepare URI BlobURI blobUri = new BlobURI() .setProfile(profileName) .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); updatedSize = ByteStreams.copy(encryptedInputStream, fbout); in1 = fbout.getSupplier().getInput(); blobUri.setEncryptionKey(Configurator.getBlobStoreDefaultEncryptionKeyAlias()); } else { in1 = in; } CloudStoreProxy.write(blobName, profileName, in1, updatedSize); return blobUri; } @Override public BlobDataSource read(final URI uri) throws IOException { InputStream in; BlobURI blobUri = new BlobURI().fromURI(uri); 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 = getCipherIVFromBlobName(BlobUtils.relativize(uri.getPath())); in = eh.decrypt(CloudStoreProxy.read(uri), Configurator.getEncryptionKey(keyAlias), iv); } catch (GeneralSecurityException gse) { throw new IOException("Unable to decrypt message blob: ", gse); } } else { in = CloudStoreProxy.read(uri); } return new BlobDataSource(uri, in); } @Override 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; } }