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.CryptoManager;
037    import org.cumulus4j.store.crypto.CryptoManagerRegistry;
038    import org.cumulus4j.store.crypto.CryptoSession;
039    import org.cumulus4j.store.crypto.Plaintext;
040    import org.cumulus4j.store.model.DataEntry;
041    import org.cumulus4j.store.model.IndexEntry;
042    import org.cumulus4j.store.model.IndexValue;
043    import org.cumulus4j.store.model.ObjectContainer;
044    import org.datanucleus.store.ExecutionContext;
045    import org.slf4j.Logger;
046    import org.slf4j.LoggerFactory;
047    
048    /**
049     * Singleton per {@link PersistenceManagerFactory} handling the encryption and decryption and thus the key management.
050     *
051     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
052     */
053    public class EncryptionHandler
054    {
055            private static final Logger logger = LoggerFactory.getLogger(EncryptionHandler.class);
056    
057            /**
058             * Dump all plain texts to the system temp directory for debug reasons. Should always be <code>false</code> in productive environments!
059             */
060            public static final boolean DEBUG_DUMP = false;
061    
062            /**
063             * Decrypt the ciphertext immediately after encryption to verify it. Should always be <code>false</code> in productive environments!
064             */
065            private static final boolean DEBUG_VERIFY_CIPHERTEXT = false;
066    
067            private static DateFormat debugDumpDateFormat;
068    
069            private static DateFormat getDebugDumpDateFormat()
070            {
071                    if (debugDumpDateFormat == null) {
072                            debugDumpDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
073                    }
074                    return debugDumpDateFormat;
075            }
076    
077            private static File debugDumpDir;
078    
079            public static File getDebugDumpDir() {
080                    if (debugDumpDir == null) {
081                            debugDumpDir = new File(new File(System.getProperty("java.io.tmpdir")), EncryptionHandler.class.getName());
082                            debugDumpDir.mkdirs();
083                    }
084    
085                    return debugDumpDir;
086            }
087    
088            public static ThreadLocal<String> debugDumpFileNameThreadLocal = new ThreadLocal<String>();
089    
090            public EncryptionHandler() { }
091    
092            private CryptoSession getCryptoSession(ExecutionContext ec)
093            {
094                    Object cryptoManagerID = ec.getProperty(CryptoManager.PROPERTY_CRYPTO_MANAGER_ID);
095                    if (cryptoManagerID == null)
096                            throw new IllegalStateException("Property \"" + CryptoManager.PROPERTY_CRYPTO_MANAGER_ID + "\" is not set!");
097    
098                    if (!(cryptoManagerID instanceof String))
099                            throw new IllegalStateException("Property \"" + CryptoManager.PROPERTY_CRYPTO_MANAGER_ID + "\" is set, but it is an instance of " + cryptoManagerID.getClass().getName() + " instead of java.lang.String!");
100    
101                    CryptoManager cryptoManager = CryptoManagerRegistry.sharedInstance(ec.getNucleusContext()).getCryptoManager((String) cryptoManagerID);
102    
103                    Object cryptoSessionID = ec.getProperty(CryptoSession.PROPERTY_CRYPTO_SESSION_ID);
104                    if (cryptoSessionID == null)
105                            throw new IllegalStateException("Property \"" + CryptoSession.PROPERTY_CRYPTO_SESSION_ID + "\" is not set!");
106    
107                    if (!(cryptoSessionID instanceof String))
108                            throw new IllegalStateException("Property \"" + CryptoSession.PROPERTY_CRYPTO_SESSION_ID + "\" is set, but it is an instance of " + cryptoSessionID.getClass().getName() + " instead of java.lang.String!");
109    
110                    CryptoSession cryptoSession = cryptoManager.getCryptoSession((String) cryptoSessionID);
111                    return cryptoSession;
112            }
113    
114            /**
115             * Get a plain (unencrypted) {@link ObjectContainer} from the encrypted byte-array in
116             * the {@link DataEntry#getValue() DataEntry.value} property.
117             * @param cryptoContext the context.
118             * @param dataEntry the {@link DataEntry} holding the encrypted data (read from).
119             * @return the plain {@link ObjectContainer}.
120             * @see #encryptDataEntry(CryptoContext, DataEntry, ObjectContainer)
121             */
122            public ObjectContainer decryptDataEntry(CryptoContext cryptoContext, DataEntry dataEntry)
123            {
124                    try {
125                            Ciphertext ciphertext = new Ciphertext();
126                            ciphertext.setKeyID(dataEntry.getKeyID());
127                            ciphertext.setData(dataEntry.getValue());
128    
129                            if (ciphertext.getData() == null)
130                                    return null; // TODO or return an empty ObjectContainer instead?
131    
132                            CryptoSession cryptoSession = getCryptoSession(cryptoContext.getExecutionContext());
133                            Plaintext plaintext = cryptoSession.decrypt(cryptoContext, ciphertext);
134                            if (plaintext == null)
135                                    throw new IllegalStateException("cryptoSession.decrypt(ciphertext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
136    
137                            ObjectContainer objectContainer;
138                            ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getData());
139                            try {
140                                    ObjectInputStream objIn = new DataNucleusObjectInputStream(in, cryptoContext.getExecutionContext().getClassLoaderResolver());
141                                    objectContainer = (ObjectContainer) objIn.readObject();
142                                    objIn.close();
143                            } catch (IOException x) {
144                                    throw new RuntimeException(x);
145                            } catch (ClassNotFoundException x) {
146                                    throw new RuntimeException(x);
147                            }
148                            return objectContainer;
149                    } catch (Exception x) {
150                            throw new RuntimeException("Failed to decrypt " + dataEntry.getClass().getSimpleName() + " with dataEntryID=" + dataEntry.getDataEntryID() + ": " + x, x);
151                    }
152            }
153    
154            /**
155             * Encrypt the given plain <code>objectContainer</code> and store the cipher-text into the given
156             * <code>dataEntry</code>.
157             * @param cryptoContext the context.
158             * @param dataEntry the {@link DataEntry} that should be holding the encrypted data (written into).
159             * @param objectContainer the plain {@link ObjectContainer} (read from).
160             * @see #decryptDataEntry(CryptoContext, DataEntry)
161             */
162            public void encryptDataEntry(CryptoContext cryptoContext, DataEntry dataEntry, ObjectContainer objectContainer)
163            {
164                    ByteArrayOutputStream out = new ByteArrayOutputStream();
165                    try {
166                            ObjectOutputStream objOut = new ObjectOutputStream(out);
167                            objOut.writeObject(objectContainer);
168                            objOut.close();
169                    } catch (IOException x) {
170                            throw new RuntimeException(x);
171                    }
172    
173                    Plaintext plaintext = new Plaintext();
174                    plaintext.setData(out.toByteArray()); out = null;
175    
176                    String debugDumpFileName = null;
177                    if (DEBUG_DUMP) {
178                            debugDumpFileName = dataEntry.getClass().getSimpleName() + "_" + dataEntry.getDataEntryID() + "_" + getDebugDumpDateFormat().format(new Date());
179                            debugDumpFileNameThreadLocal.set(debugDumpFileName);
180                            try {
181                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".plain"));
182                                    fout.write(plaintext.getData());
183                                    fout.close();
184                            } catch (IOException e) {
185                                    logger.error("encryptDataEntry: Dumping plaintext failed: " + e, e);
186                            }
187                    }
188    
189                    CryptoSession cryptoSession = getCryptoSession(cryptoContext.getExecutionContext());
190                    Ciphertext ciphertext = cryptoSession.encrypt(cryptoContext, plaintext);
191    
192                    if (ciphertext == null)
193                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
194    
195                    if (ciphertext.getKeyID() < 0)
196                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned a ciphertext with keyID < 0! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
197    
198                    if (DEBUG_DUMP) {
199                            try {
200                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".crypt"));
201                                    fout.write(ciphertext.getData());
202                                    fout.close();
203                            } catch (IOException e) {
204                                    logger.error("encryptDataEntry: Dumping ciphertext failed: " + e, e);
205                            }
206                    }
207    
208                    if (DEBUG_VERIFY_CIPHERTEXT) {
209                            try {
210                                    Plaintext decrypted = cryptoSession.decrypt(cryptoContext, ciphertext);
211                                    if (!Arrays.equals(decrypted.getData(), plaintext.getData()))
212                                            throw new IllegalStateException("decrypted != plaintext");
213                            } catch (Exception x) {
214                                    throw new RuntimeException("Verification of ciphertext failed (see dumps in \"" + debugDumpFileName + ".*\"): ", x);
215                            }
216                    }
217    
218                    dataEntry.setKeyID(ciphertext.getKeyID());
219                    dataEntry.setValue(ciphertext.getData());
220            }
221    
222            /**
223             * Get a plain (unencrypted) {@link IndexValue} from the encrypted byte-array in
224             * the {@link IndexEntry#getIndexValue() IndexEntry.indexValue} property.
225             * @param cryptoContext the context.
226             * @param indexEntry the {@link IndexEntry} holding the encrypted data (read from).
227             * @return the plain {@link IndexValue}.
228             */
229            public IndexValue decryptIndexEntry(CryptoContext cryptoContext, IndexEntry indexEntry)
230            {
231                    try {
232                            Ciphertext ciphertext = new Ciphertext();
233                            ciphertext.setKeyID(indexEntry.getKeyID());
234                            ciphertext.setData(indexEntry.getIndexValue());
235    
236                            Plaintext plaintext = null;
237                            if (ciphertext.getData() != null) {
238                                    CryptoSession cryptoSession = getCryptoSession(cryptoContext.getExecutionContext());
239                                    plaintext = cryptoSession.decrypt(cryptoContext, ciphertext);
240                                    if (plaintext == null)
241                                            throw new IllegalStateException("cryptoSession.decrypt(ciphertext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
242                            }
243    
244                            IndexValue indexValue = new IndexValue(plaintext == null ? null : plaintext.getData());
245                            return indexValue;
246                    } catch (Exception x) {
247                            throw new RuntimeException("Failed to decrypt " + indexEntry.getClass().getSimpleName() + " with indexEntryID=" + indexEntry.getIndexEntryID() + ": " + x, x);
248                    }
249            }
250    
251            /**
252             * Encrypt the given plain <code>indexValue</code> and store the cipher-text into the given
253             * <code>indexEntry</code>.
254             * @param cryptoContext the context.
255             * @param indexEntry the {@link IndexEntry} that should be holding the encrypted data (written into).
256             * @param indexValue the plain {@link IndexValue} (read from).
257             */
258            public void encryptIndexEntry(CryptoContext cryptoContext, IndexEntry indexEntry, IndexValue indexValue)
259            {
260                    Plaintext plaintext = new Plaintext();
261                    plaintext.setData(indexValue.toByteArray());
262    
263                    String debugDumpFileName = null;
264                    if (DEBUG_DUMP) {
265                            debugDumpFileName = indexEntry.getClass().getSimpleName() + "_" + indexEntry.getIndexEntryID() + "_" + getDebugDumpDateFormat().format(new Date());
266                            debugDumpFileNameThreadLocal.set(debugDumpFileName);
267                            try {
268                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".plain"));
269                                    fout.write(plaintext.getData());
270                                    fout.close();
271                            } catch (IOException e) {
272                                    logger.error("encryptIndexEntry: Dumping plaintext failed: " + e, e);
273                            }
274                    }
275    
276                    CryptoSession cryptoSession = getCryptoSession(cryptoContext.getExecutionContext());
277                    Ciphertext ciphertext = cryptoSession.encrypt(cryptoContext, plaintext);
278    
279                    if (ciphertext == null)
280                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned null! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
281    
282                    if (ciphertext.getKeyID() < 0)
283                            throw new IllegalStateException("cryptoSession.encrypt(plaintext) returned a ciphertext with keyID < 0! cryptoManagerID=" + cryptoSession.getCryptoManager().getCryptoManagerID() + " cryptoSessionID=" + cryptoSession.getCryptoSessionID());
284    
285                    if (DEBUG_DUMP) {
286                            try {
287                                    FileOutputStream fout = new FileOutputStream(new File(getDebugDumpDir(), debugDumpFileName + ".crypt"));
288                                    fout.write(ciphertext.getData());
289                                    fout.close();
290                            } catch (IOException e) {
291                                    logger.error("encryptIndexEntry: Dumping ciphertext failed: " + e, e);
292                            }
293                    }
294    
295                    if (DEBUG_VERIFY_CIPHERTEXT) {
296                            try {
297                                    Plaintext decrypted = cryptoSession.decrypt(cryptoContext, ciphertext);
298                                    if (!Arrays.equals(decrypted.getData(), plaintext.getData()))
299                                            throw new IllegalStateException("decrypted != plaintext");
300                            } catch (Exception x) {
301                                    throw new RuntimeException("Verification of ciphertext failed (see plaintext in file \"" + debugDumpFileName + "\"): ", x);
302                            }
303                    }
304    
305                    indexEntry.setKeyID(ciphertext.getKeyID());
306                    indexEntry.setIndexValue(ciphertext.getData());
307            }
308    }