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 }