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 }