001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mime4j.storage;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.security.GeneralSecurityException;
025import java.security.NoSuchAlgorithmException;
026
027import javax.crypto.Cipher;
028import javax.crypto.CipherInputStream;
029import javax.crypto.CipherOutputStream;
030import javax.crypto.KeyGenerator;
031import javax.crypto.spec.SecretKeySpec;
032
033/**
034 * A {@link StorageProvider} that transparently scrambles and unscrambles the
035 * data stored by another <code>StorageProvider</code>.
036 *
037 * <p>
038 * Example usage:
039 *
040 * <pre>
041 * StorageProvider mistrusted = new TempFileStorageProvider();
042 * StorageProvider enciphered = new CipherStorageProvider(mistrusted);
043 * StorageProvider provider = new ThresholdStorageProvider(enciphered);
044 * DefaultStorageProvider.setInstance(provider);
045 * </pre>
046 */
047public class CipherStorageProvider extends AbstractStorageProvider {
048
049    private final StorageProvider backend;
050    private final String algorithm;
051    private final KeyGenerator keygen;
052
053    /**
054     * Creates a new <code>CipherStorageProvider</code> for the given back-end
055     * using the Blowfish cipher algorithm.
056     *
057     * @param backend
058     *            back-end storage strategy to encrypt.
059     */
060    public CipherStorageProvider(StorageProvider backend) {
061        this(backend, "Blowfish");
062    }
063
064    /**
065     * Creates a new <code>CipherStorageProvider</code> for the given back-end
066     * and cipher algorithm.
067     *
068     * @param backend
069     *            back-end storage strategy to encrypt.
070     * @param algorithm
071     *            the name of the symmetric block cipher algorithm such as
072     *            "Blowfish", "AES" or "RC2".
073     */
074    public CipherStorageProvider(StorageProvider backend, String algorithm) {
075        if (backend == null)
076            throw new IllegalArgumentException();
077
078        try {
079            this.backend = backend;
080            this.algorithm = algorithm;
081            this.keygen = KeyGenerator.getInstance(algorithm);
082        } catch (NoSuchAlgorithmException e) {
083            throw new IllegalArgumentException(e);
084        }
085    }
086
087    public StorageOutputStream createStorageOutputStream() throws IOException {
088        SecretKeySpec skeySpec = getSecretKeySpec();
089
090        return new CipherStorageOutputStream(backend
091                .createStorageOutputStream(), algorithm, skeySpec);
092    }
093
094    private SecretKeySpec getSecretKeySpec() {
095        byte[] raw = keygen.generateKey().getEncoded();
096        return new SecretKeySpec(raw, algorithm);
097    }
098
099    private static final class CipherStorageOutputStream extends
100            StorageOutputStream {
101        private final StorageOutputStream storageOut;
102        private final String algorithm;
103        private final SecretKeySpec skeySpec;
104        private final CipherOutputStream cipherOut;
105
106        public CipherStorageOutputStream(StorageOutputStream out,
107                String algorithm, SecretKeySpec skeySpec) throws IOException {
108            try {
109                this.storageOut = out;
110                this.algorithm = algorithm;
111                this.skeySpec = skeySpec;
112
113                Cipher cipher = Cipher.getInstance(algorithm);
114                cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
115
116                this.cipherOut = new CipherOutputStream(out, cipher);
117            } catch (GeneralSecurityException e) {
118                throw (IOException) new IOException().initCause(e);
119            }
120        }
121
122        @Override
123        public void close() throws IOException {
124            super.close();
125            cipherOut.close();
126        }
127
128        @Override
129        protected void write0(byte[] buffer, int offset, int length)
130                throws IOException {
131            cipherOut.write(buffer, offset, length);
132        }
133
134        @Override
135        protected Storage toStorage0() throws IOException {
136            // cipherOut has already been closed because toStorage calls close
137            Storage encrypted = storageOut.toStorage();
138            return new CipherStorage(encrypted, algorithm, skeySpec);
139        }
140    }
141
142    private static final class CipherStorage implements Storage {
143        private Storage encrypted;
144        private final String algorithm;
145        private final SecretKeySpec skeySpec;
146
147        public CipherStorage(Storage encrypted, String algorithm,
148                SecretKeySpec skeySpec) {
149            this.encrypted = encrypted;
150            this.algorithm = algorithm;
151            this.skeySpec = skeySpec;
152        }
153
154        public void delete() {
155            if (encrypted != null) {
156                encrypted.delete();
157                encrypted = null;
158            }
159        }
160
161        public InputStream getInputStream() throws IOException {
162            if (encrypted == null)
163                throw new IllegalStateException("storage has been deleted");
164
165            try {
166                Cipher cipher = Cipher.getInstance(algorithm);
167                cipher.init(Cipher.DECRYPT_MODE, skeySpec);
168
169                InputStream in = encrypted.getInputStream();
170                return new CipherInputStream(in, cipher);
171            } catch (GeneralSecurityException e) {
172                throw (IOException) new IOException().initCause(e);
173            }
174        }
175    }
176
177}