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 }