001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.store;
019    
020    import java.io.ByteArrayInputStream;
021    import java.io.ByteArrayOutputStream;
022    import java.io.File;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    import java.io.ObjectInputStream;
026    import java.io.ObjectOutputStream;
027    import java.text.DateFormat;
028    import java.text.SimpleDateFormat;
029    import java.util.Arrays;
030    import java.util.Date;
031    
032    import javax.jdo.PersistenceManagerFactory;
033    
034    import org.cumulus4j.store.crypto.Ciphertext;
035    import org.cumulus4j.store.crypto.CryptoContext;
036    import org.cumulus4j.store.crypto.CryptoSession;
037    import org.cumulus4j.store.crypto.Plaintext;
038    import org.cumulus4j.store.model.DataEntry;
039    import org.cumulus4j.store.model.IndexEntry;
040    import org.cumulus4j.store.model.IndexValue;
041    import org.cumulus4j.store.model.ObjectContainer;
042    import org.slf4j.Logger;
043    import org.slf4j.LoggerFactory;
044    
045    /**
046     * Singleton per {@link PersistenceManagerFactory} handling the encryption and decryption and thus the key management.
047     *
048     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
049     */
050    public class EncryptionHandler
051    {
052            private static final Logger logger = LoggerFactory.getLogger(EncryptionHandler.class);
053    
054            /**
055             * Dump all plain texts to the system temp directory for debug reasons. Should always be <code>false</code> in productive environments!
056             */
057            public static final boolean DEBUG_DUMP = false;
058    
059            /**
060             * Decrypt the ciphertext immediately after encryption to verify it. Should always be <code>false</code> in productive environments!
061             */
062            private static final boolean DEBUG_VERIFY_CIPHERTEXT = false;
063    
064            private static DateFormat debugDumpDateFormat;
065    
066            private static DateFormat getDebugDumpDateFormat()
067            {
068                    if (debugDumpDateFormat == null) {
069                            debugDumpDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
070                    }
071                    return debugDumpDateFormat;
072            }
073    
074            private static File debugDumpDir;
075    
076            public static File getDebugDumpDir() {
077                    if (debugDumpDir == null) {
078                            debugDumpDir = new File(new File(System.getProperty("java.io.tmpdir")), EncryptionHandler.class.getName());
079                            debugDumpDir.mkdirs();
080                    }
081    
082                    return debugDumpDir;
083            }
084    
085            public static ThreadLocal<String> debugDumpFileNameThreadLocal = new ThreadLocal<String>();
086    
087            public EncryptionHandler() { }
088    
089            /**
090             * Get a plain (unencrypted) {@link ObjectContainer} from the encrypted byte-array in
091             * the {@link DataEntry#getValue() DataEntry.value} property.
092             * @param cryptoContext the context.
093             * @param dataEntry the {@link DataEntry} holding the encrypted data (read from).
094             * @return the plain {@link ObjectContainer}.
095             * @see #encryptDataEntry(CryptoContext, DataEntry, ObjectContainer)
096             */
097            public ObjectContainer decryptDataEntry(CryptoContext cryptoContext, DataEntry dataEntry)
098            {
099                    try {
100                            Ciphertext ciphertext = new Ciphertext();
101                            ciphertext.setKeyID(dataEntry.getKeyID());
102                            ciphertext.setData(dataEntry.getValue());
103    
104                            if (ciphertext.getData() == null)
105                                    return null; // TODO or return an empty ObjectContainer instead?
106    
107                            CryptoSession cryptoSession = cryptoContext.getCryptoSession();
108                            Plaintext plaintext = cryptoSession.decrypt(cryptoContext, ciphertext);
109                            if (plaintext == null)
110                                    throw new IllegalStateException("cryptoSession.decrypt(ciphertext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
111    
112                            ObjectContainer objectContainer;
113                            ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getData());
114                            try {
115                                    ObjectInputStream objIn = new DataNucleusObjectInputStream(in, cryptoContext.getExecutionContext().getClassLoaderResolver());
116                                    objectContainer = (ObjectContainer) objIn.readObject();
117                                    objIn.close();
118                            } catch (IOException x) {
119                                    throw new RuntimeException(x);
120                            } catch (ClassNotFoundException x) {
121                                    throw new RuntimeException(x);
122                            }
123                            return objectContainer;
124                    } catch (Exception x) {
125                            throw new RuntimeException("Failed to decrypt " + dataEntry.getClass().getSimpleName() + " with dataEntryID=" + dataEntry.getDataEntryID() + ": " + x, x);
126                    }
127            }
128    
129            /**
130             * Encrypt the given plain <code>objectContainer</code> and store the cipher-text into the given
131             * <code>dataEntry</code>.
132             * @param cryptoContext the context.
133             * @param dataEntry the {@link DataEntry} that should be holding the encrypted data (written into).
134             * @param objectContainer the plain {@link ObjectContainer} (read from).
135             * @see #decryptDataEntry(CryptoContext, DataEntry)
136             */
137            public void encryptDataEntry(CryptoContext cryptoContext, DataEntry dataEntry, ObjectContainer objectContainer)
138            {
139                    ByteArrayOutputStream out = new ByteArrayOutputStream();
140                    try {
141                            ObjectOutputStream objOut = new ObjectOutputStream(out);
142                            objOut.writeObject(objectContainer);
143                            objOut.close();
144                    } catch (IOException x) {
145                            throw new RuntimeException(x);
146                    }
147    
148                    Plaintext plaintext = new Plaintext();
149                    plaintext.setData(out.toByteArray()); out = null;
150    
151                    String debugDumpFileName = null;
152                    if (DEBUG_DUMP) {
153                            debugDumpFileName = dataEntry.getClass().getSimpleName() + "_" + dataEntry.getDataEntryID() + "_" + getDebugDumpDateFormat().format(new Date());
154                            debugDumpFileNameThreadLocal.set(debugDumpFileName);
155                            try {
156                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".plain"));
157                                    fout.write(plaintext.getData());
158                                    fout.close();
159                            } catch (IOException e) {
160                                    logger.error("encryptDataEntry: Dumping plaintext failed: " + e, e);
161                            }
162                    }
163    
164                    CryptoSession cryptoSession = cryptoContext.getCryptoSession();
165                    Ciphertext ciphertext = cryptoSession.encrypt(cryptoContext, plaintext);
166    
167                    if (ciphertext == null)
168                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
169    
170                    if (ciphertext.getKeyID() < 0)
171                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned a ciphertext with keyID < 0! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
172    
173                    if (DEBUG_DUMP) {
174                            try {
175                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".crypt"));
176                                    fout.write(ciphertext.getData());
177                                    fout.close();
178                            } catch (IOException e) {
179                                    logger.error("encryptDataEntry: Dumping ciphertext failed: " + e, e);
180                            }
181                    }
182    
183                    if (DEBUG_VERIFY_CIPHERTEXT) {
184                            try {
185                                    Plaintext decrypted = cryptoSession.decrypt(cryptoContext, ciphertext);
186                                    if (!Arrays.equals(decrypted.getData(), plaintext.getData()))
187                                            throw new IllegalStateException("decrypted != plaintext");
188                            } catch (Exception x) {
189                                    throw new RuntimeException("Verification of ciphertext failed (see dumps in \"" + debugDumpFileName + ".*\"): ", x);
190                            }
191                    }
192    
193                    dataEntry.setKeyID(ciphertext.getKeyID());
194                    dataEntry.setValue(ciphertext.getData());
195            }
196    
197            /**
198             * Get a plain (unencrypted) {@link IndexValue} from the encrypted byte-array in
199             * the {@link IndexEntry#getIndexValue() IndexEntry.indexValue} property.
200             * @param cryptoContext the context.
201             * @param indexEntry the {@link IndexEntry} holding the encrypted data (read from).
202             * @return the plain {@link IndexValue}.
203             */
204            public IndexValue decryptIndexEntry(CryptoContext cryptoContext, IndexEntry indexEntry)
205            {
206                    try {
207                            Ciphertext ciphertext = new Ciphertext();
208                            ciphertext.setKeyID(indexEntry.getKeyID());
209                            ciphertext.setData(indexEntry.getIndexValue());
210    
211                            Plaintext plaintext = null;
212                            if (ciphertext.getData() != null) {
213                                    CryptoSession cryptoSession = cryptoContext.getCryptoSession();
214                                    plaintext = cryptoSession.decrypt(cryptoContext, ciphertext);
215                                    if (plaintext == null)
216                                            throw new IllegalStateException("cryptoSession.decrypt(ciphertext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
217                            }
218    
219                            IndexValue indexValue = new IndexValue(plaintext == null ? null : plaintext.getData());
220                            return indexValue;
221                    } catch (Exception x) {
222                            throw new RuntimeException("Failed to decrypt " + indexEntry.getClass().getSimpleName() + " with indexEntryID=" + indexEntry.getIndexEntryID() + ": " + x, x);
223                    }
224            }
225    
226            /**
227             * Encrypt the given plain <code>indexValue</code> and store the cipher-text into the given
228             * <code>indexEntry</code>.
229             * @param cryptoContext the context.
230             * @param indexEntry the {@link IndexEntry} that should be holding the encrypted data (written into).
231             * @param indexValue the plain {@link IndexValue} (read from).
232             */
233            public void encryptIndexEntry(CryptoContext cryptoContext, IndexEntry indexEntry, IndexValue indexValue)
234            {
235                    Plaintext plaintext = new Plaintext();
236                    plaintext.setData(indexValue.toByteArray());
237    
238                    String debugDumpFileName = null;
239                    if (DEBUG_DUMP) {
240                            debugDumpFileName = indexEntry.getClass().getSimpleName() + "_" + indexEntry.getIndexEntryID() + "_" + getDebugDumpDateFormat().format(new Date());
241                            debugDumpFileNameThreadLocal.set(debugDumpFileName);
242                            try {
243                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".plain"));
244                                    fout.write(plaintext.getData());
245                                    fout.close();
246                            } catch (IOException e) {
247                                    logger.error("encryptIndexEntry: Dumping plaintext failed: " + e, e);
248                            }
249                    }
250    
251                    CryptoSession cryptoSession = cryptoContext.getCryptoSession();
252                    Ciphertext ciphertext = cryptoSession.encrypt(cryptoContext, plaintext);
253    
254                    if (ciphertext == null)
255                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
256    
257                    if (ciphertext.getKeyID() < 0)
258                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned a ciphertext with keyID < 0! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
259    
260                    if (DEBUG_DUMP) {
261                            try {
262                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".crypt"));
263                                    fout.write(ciphertext.getData());
264                                    fout.close();
265                            } catch (IOException e) {
266                                    logger.error("encryptIndexEntry: Dumping ciphertext failed: " + e, e);
267                            }
268                    }
269    
270                    if (DEBUG_VERIFY_CIPHERTEXT) {
271                            try {
272                                    Plaintext decrypted = cryptoSession.decrypt(cryptoContext, ciphertext);
273                                    if (!Arrays.equals(decrypted.getData(), plaintext.getData()))
274                                            throw new IllegalStateException("decrypted != plaintext");
275                            } catch (Exception x) {
276                                    throw new RuntimeException("Verification of ciphertext failed (see plaintext in file \"" + debugDumpFileName + "\"): ", x);
277                            }
278                    }
279    
280                    indexEntry.setKeyID(ciphertext.getKeyID());
281                    indexEntry.setIndexValue(ciphertext.getData());
282            }
283    }