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 }