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.crypto.keymanager; 019 020 import java.lang.ref.WeakReference; 021 import java.security.NoSuchAlgorithmException; 022 import java.security.SecureRandom; 023 import java.util.Collections; 024 import java.util.Date; 025 import java.util.HashMap; 026 import java.util.Iterator; 027 import java.util.LinkedList; 028 import java.util.List; 029 import java.util.Map; 030 import java.util.Timer; 031 import java.util.TimerTask; 032 033 import javax.crypto.NoSuchPaddingException; 034 035 import org.bouncycastle.crypto.AsymmetricCipherKeyPair; 036 import org.bouncycastle.crypto.AsymmetricCipherKeyPairGenerator; 037 import org.bouncycastle.crypto.params.KeyParameter; 038 import org.bouncycastle.crypto.params.ParametersWithIV; 039 import org.cumulus4j.crypto.Cipher; 040 import org.cumulus4j.crypto.CipherOperationMode; 041 import org.cumulus4j.crypto.CryptoRegistry; 042 import org.cumulus4j.store.crypto.AbstractCryptoManager; 043 import org.cumulus4j.store.crypto.CryptoManagerRegistry; 044 import org.datanucleus.NucleusContext; 045 import org.datanucleus.PersistenceConfiguration; 046 import org.slf4j.Logger; 047 import org.slf4j.LoggerFactory; 048 049 /** 050 * <p> 051 * Cache for secret keys, {@link Cipher}s and other crypto-related objects. 052 * </p><p> 053 * There exists one instance of <code>CryptoCache</code> per {@link KeyManagerCryptoManager}. 054 * This cache therefore holds objects across multiple {@link KeyManagerCryptoSession sessions}. 055 * </p> 056 * 057 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 058 */ 059 public class CryptoCache 060 { 061 private static final Logger logger = LoggerFactory.getLogger(CryptoCache.class); 062 063 private SecureRandom random = new SecureRandom(); 064 private long activeEncryptionKeyID = -1; 065 private Date activeEncryptionKeyUntilExcl = null; 066 private Object activeEncryptionKeyMutex = new Object(); 067 068 private Map<Long, CryptoCacheKeyEntry> keyID2key = Collections.synchronizedMap(new HashMap<Long, CryptoCacheKeyEntry>()); 069 070 private Map<CipherOperationMode, Map<String, Map<Long, List<CryptoCacheCipherEntry>>>> opmode2cipherTransformation2keyID2cipherEntries = Collections.synchronizedMap( 071 new HashMap<CipherOperationMode, Map<String,Map<Long,List<CryptoCacheCipherEntry>>>>() 072 ); 073 074 private KeyManagerCryptoManager cryptoManager; 075 076 /** 077 * Create a <code>CryptoCache</code> instance. 078 * @param cryptoManager the owning <code>CryptoManager</code>. 079 */ 080 public CryptoCache(KeyManagerCryptoManager cryptoManager) 081 { 082 if (cryptoManager == null) 083 throw new IllegalArgumentException("cryptoManager == null"); 084 085 this.cryptoManager = cryptoManager; 086 } 087 088 /** 089 * Get the currently active encryption key. If there has none yet be {@link #setActiveEncryptionKeyID(long, Date) set} 090 * or the <code>activeUntilExcl</code> has been reached (i.e. the previous active key expired), 091 * this method returns -1. 092 * @return the currently active encryption key or -1, if there is none. 093 * @see #setActiveEncryptionKeyID(long, Date) 094 */ 095 public long getActiveEncryptionKeyID() 096 { 097 long activeEncryptionKeyID; 098 Date activeEncryptionKeyUntilExcl; 099 synchronized (activeEncryptionKeyMutex) { 100 activeEncryptionKeyID = this.activeEncryptionKeyID; 101 activeEncryptionKeyUntilExcl = this.activeEncryptionKeyUntilExcl; 102 } 103 104 if (activeEncryptionKeyUntilExcl == null) 105 return -1; 106 107 if (activeEncryptionKeyUntilExcl.compareTo(new Date()) <= 0) 108 return -1; 109 110 return activeEncryptionKeyID; 111 } 112 113 /** 114 * Set the currently active encryption key. 115 * @param activeEncryptionKeyID identifier of the symmetric secret key that is currently active. 116 * @param activeUntilExcl timestamp until when (excluding) the specified key is active. 117 * @see #getActiveEncryptionKeyID() 118 */ 119 public void setActiveEncryptionKeyID(long activeEncryptionKeyID, Date activeUntilExcl) 120 { 121 if (activeEncryptionKeyID <= 0) 122 throw new IllegalArgumentException("activeEncryptionKeyID <= 0"); 123 124 if (activeUntilExcl == null) 125 throw new IllegalArgumentException("activeUntilExcl == null"); 126 127 synchronized (activeEncryptionKeyMutex) { 128 this.activeEncryptionKeyID = activeEncryptionKeyID; 129 this.activeEncryptionKeyUntilExcl = activeUntilExcl; 130 } 131 } 132 133 /** 134 * Get the actual key data for the given key identifier. 135 * @param keyID identifier of the requested key. 136 * @return actual key data or <code>null</code>, if the specified key is not cached. 137 */ 138 protected byte[] getKeyData(long keyID) 139 { 140 CryptoCacheKeyEntry entry = keyID2key.get(keyID); 141 if (entry == null) { 142 if (logger.isTraceEnabled()) logger.trace("getKeyData: No cached key with keyID={} found.", keyID); 143 return null; 144 } 145 else { 146 if (logger.isTraceEnabled()) logger.trace("getKeyData: Found cached key with keyID={}.", keyID); 147 return entry.getKeyData(); 148 } 149 } 150 151 /** 152 * Put a certain key into this cache. 153 * @param keyID identifier of the key. Must be <= 0. 154 * @param keyData actual key. Must not be <code>null</code>. 155 * @return the immutable entry for the given key in this cache. 156 */ 157 protected CryptoCacheKeyEntry setKeyData(long keyID, byte[] keyData) 158 { 159 CryptoCacheKeyEntry entry = new CryptoCacheKeyEntry(keyID, keyData); 160 keyID2key.put(keyID, entry); 161 return entry; 162 } 163 164 /** 165 * <p> 166 * Acquire a decrypter and {@link Cipher#init(CipherOperationMode, org.bouncycastle.crypto.CipherParameters) initialise} it so that 167 * it is ready to be used. 168 * </p><p> 169 * This method can only return a <code>Cipher</code>, if there is one cached, already, or at least the key is cached so that a new 170 * <code>Cipher</code> can be created. If there is neither a cipher nor a key cached, this method returns <code>null</code>. 171 * The key - if found - is refreshed (with the current timestamp) by this operation and will thus be evicted later. 172 * </p><p> 173 * <b>Important:</b> You must use a try-finally-block ensuring that {@link #releaseCipherEntry(CryptoCacheCipherEntry)} is called! 174 * </p> 175 * 176 * @param cipherTransformation the encryption algorithm (the complete transformation as passed to {@link CryptoRegistry#createCipher(String)}). 177 * @param keyID identifier of the key. 178 * @param iv initialisation vector. Must be the same as the one that was used for encryption. 179 * @return <code>null</code> or an entry wrapping the desired cipher. 180 * @see #acquireDecrypter(String, long, byte[], byte[]) 181 * @see #releaseCipherEntry(CryptoCacheCipherEntry) 182 */ 183 public CryptoCacheCipherEntry acquireDecrypter(String cipherTransformation, long keyID, byte[] iv) 184 { 185 return acquireDecrypter(cipherTransformation, keyID, null, iv); 186 } 187 188 /** 189 * <p> 190 * Acquire a decrypter and {@link Cipher#init(CipherOperationMode, org.bouncycastle.crypto.CipherParameters) initialise} it so that 191 * it is ready to be used. 192 * </p><p> 193 * This method returns an existing <code>Cipher</code>, if there is one cached, already. Otherwise a new <code>Cipher</code> is created. 194 * The key is added (with the current timestamp) into the cache. 195 * </p><p> 196 * <b>Important:</b> You must use a try-finally-block ensuring that {@link #releaseCipherEntry(CryptoCacheCipherEntry)} is called! 197 * </p> 198 * 199 * @param encryptionAlgorithm the encryption algorithm (the complete transformation as passed to {@link CryptoRegistry#createCipher(String)}). 200 * @param keyID identifier of the key. 201 * @param keyData the actual key. If it is <code>null</code>, the key is fetched from the cache. If it is not cached, 202 * this method returns <code>null</code>. 203 * @param iv initialisation vector. Must be the same as the one that was used for encryption. 204 * @return an entry wrapping the desired cipher. Never returns <code>null</code>, if <code>keyData</code> was specified. 205 * If <code>keyData == null</code> and the key is not cached, <code>null</code> is returned. 206 * @see #acquireDecrypter(String, long, byte[]) 207 * @see #releaseCipherEntry(CryptoCacheCipherEntry) 208 */ 209 public CryptoCacheCipherEntry acquireDecrypter(String encryptionAlgorithm, long keyID, byte[] keyData, byte[] iv) 210 { 211 return acquireCipherEntry(CipherOperationMode.DECRYPT, encryptionAlgorithm, keyID, keyData, iv); 212 } 213 214 /** 215 * <p> 216 * Acquire an encrypter and {@link Cipher#init(CipherOperationMode, org.bouncycastle.crypto.CipherParameters) initialise} it so that 217 * it is ready to be used. 218 * </p><p> 219 * This method can only return a <code>Cipher</code>, if there is one cached, already, or at least the key is cached so that a new 220 * <code>Cipher</code> can be created. If there is neither a cipher nor a key cached, this method returns <code>null</code>. 221 * The key - if found - is refreshed (with the current timestamp) by this operation and will thus be evicted later. 222 * </p><p> 223 * You should use a try-finally-block ensuring that {@link #releaseCipherEntry(CryptoCacheCipherEntry)} is called! 224 * </p><p> 225 * This method generates a random IV (initialisation vector) every time it is called. The IV can be obtained via 226 * {@link Cipher#getParameters()} and casting the result to {@link ParametersWithIV}. The IV is required for decryption. 227 * </p> 228 * 229 * @param encryptionAlgorithm the encryption algorithm (the complete transformation as passed to {@link CryptoRegistry#createCipher(String)}). 230 * @param keyID identifier of the key. 231 * @return <code>null</code> or an entry wrapping the desired cipher. 232 * @see #acquireEncrypter(String, long, byte[]) 233 * @see #releaseCipherEntry(CryptoCacheCipherEntry) 234 */ 235 public CryptoCacheCipherEntry acquireEncrypter(String encryptionAlgorithm, long keyID) 236 { 237 return acquireEncrypter(encryptionAlgorithm, keyID, null); 238 } 239 240 /** 241 * <p> 242 * Acquire an encrypter and {@link Cipher#init(CipherOperationMode, org.bouncycastle.crypto.CipherParameters) initialise} it so that 243 * it is ready to be used. 244 * </p><p> 245 * This method returns an existing <code>Cipher</code>, if there is one cached, already. Otherwise a new <code>Cipher</code> is created. 246 * The key is added (with the current timestamp) into the cache. 247 * </p><p> 248 * You should use a try-finally-block ensuring that {@link #releaseCipherEntry(CryptoCacheCipherEntry)} is called! 249 * </p><p> 250 * This method generates a random IV (initialisation vector) every time it is called. The IV can be obtained via 251 * {@link Cipher#getParameters()} and casting the result to {@link ParametersWithIV}. The IV is required for decryption. 252 * </p> 253 * 254 * @param cipherTransformation the encryption algorithm (the complete transformation as passed to {@link CryptoRegistry#createCipher(String)}). 255 * @param keyID identifier of the key. 256 * @param keyData the actual key. If it is <code>null</code>, the key is fetched from the cache. If it is not cached, 257 * this method returns <code>null</code>. 258 * @return an entry wrapping the desired cipher. Never returns <code>null</code>, if <code>keyData</code> was specified. 259 * If <code>keyData == null</code> and the key is not cached, <code>null</code> is returned. 260 * @see #acquireEncrypter(String, long) 261 * @see #releaseCipherEntry(CryptoCacheCipherEntry) 262 */ 263 public CryptoCacheCipherEntry acquireEncrypter(String cipherTransformation, long keyID, byte[] keyData) 264 { 265 return acquireCipherEntry(CipherOperationMode.ENCRYPT, cipherTransformation, keyID, keyData, null); 266 } 267 268 private CryptoCacheCipherEntry acquireCipherEntry( 269 CipherOperationMode opmode, String cipherTransformation, long keyID, byte[] keyData, byte[] iv 270 ) 271 { 272 try { 273 Map<String, Map<Long, List<CryptoCacheCipherEntry>>> cipherTransformation2keyID2encrypters = 274 opmode2cipherTransformation2keyID2cipherEntries.get(opmode); 275 276 if (cipherTransformation2keyID2encrypters != null) { 277 Map<Long, List<CryptoCacheCipherEntry>> keyID2Encrypters = cipherTransformation2keyID2encrypters.get(cipherTransformation); 278 if (keyID2Encrypters != null) { 279 List<CryptoCacheCipherEntry> encrypters = keyID2Encrypters.get(keyID); 280 if (encrypters != null) { 281 CryptoCacheCipherEntry entry = popOrNull(encrypters); 282 if (entry != null) { 283 entry = new CryptoCacheCipherEntry( 284 setKeyData(keyID, entry.getKeyEntry().getKeyData()), entry 285 ); 286 if (iv == null) { 287 iv = new byte[entry.getCipher().getIVSize()]; 288 random.nextBytes(iv); 289 } 290 291 if (logger.isTraceEnabled()) 292 logger.trace( 293 "acquireCipherEntry: Found cached Cipher@{} for opmode={}, encryptionAlgorithm={} and keyID={}. Initialising it with new IV (without key).", 294 new Object[] { System.identityHashCode(entry.getCipher()), opmode, cipherTransformation, keyID } 295 ); 296 297 entry.getCipher().init( 298 opmode, 299 new ParametersWithIV(null, iv) // no key, because we reuse the cipher and want to suppress expensive rekeying 300 ); 301 return entry; 302 } 303 } 304 } 305 } 306 307 if (keyData == null) { 308 keyData = getKeyData(keyID); 309 if (keyData == null) 310 return null; 311 } 312 313 Cipher cipher; 314 try { 315 cipher = CryptoRegistry.sharedInstance().createCipher(cipherTransformation); 316 } catch (NoSuchAlgorithmException e) { 317 throw new RuntimeException(e); 318 } catch (NoSuchPaddingException e) { 319 throw new RuntimeException(e); 320 } 321 322 CryptoCacheCipherEntry entry = new CryptoCacheCipherEntry( 323 setKeyData(keyID, keyData), cipherTransformation, cipher 324 ); 325 if (iv == null) { 326 iv = new byte[entry.getCipher().getIVSize()]; 327 random.nextBytes(iv); 328 } 329 330 if (logger.isTraceEnabled()) 331 logger.trace( 332 "acquireCipherEntry: Created new Cipher@{} for opmode={}, encryptionAlgorithm={} and keyID={}. Initialising it with key and IV.", 333 new Object[] { System.identityHashCode(entry.getCipher()), opmode, cipherTransformation, keyID } 334 ); 335 336 entry.getCipher().init( 337 opmode, 338 new ParametersWithIV(new KeyParameter(keyData), iv) // with key, because 1st time we use this cipher 339 ); 340 return entry; 341 } finally { 342 // We do this at the end in order to maybe still fetch an entry that is about to expire just right now. 343 // Otherwise it might happen, that we delete one and recreate it again instead of just reusing it. Marco :-) 344 initTimerTaskOrRemoveExpiredEntriesPeriodically(); 345 } 346 } 347 348 /** 349 * <p> 350 * Release a {@link Cipher} wrapped in the given entry. 351 * </p><p> 352 * This should be called in a finally block ensuring that the Cipher is put back into the cache. 353 * </p> 354 * @param cipherEntry the entry to be put back into the cache or <code>null</code>, if it was not yet assigned. 355 * This method accepts <code>null</code> as argument to make usage in a try-finally-block easier and less error-prone 356 * (no <code>null</code>-checks required). 357 * @see #acquireDecrypter(String, long, byte[]) 358 * @see #acquireDecrypter(String, long, byte[], byte[]) 359 * @see #acquireEncrypter(String, long) 360 * @see #acquireEncrypter(String, long, byte[]) 361 */ 362 public void releaseCipherEntry(CryptoCacheCipherEntry cipherEntry) 363 { 364 if (cipherEntry == null) 365 return; 366 367 if (logger.isTraceEnabled()) 368 logger.trace( 369 "releaseCipherEntry: Releasing Cipher@{} for opmode={}, encryptionAlgorithm={} keyID={}.", 370 new Object[] { 371 System.identityHashCode(cipherEntry.getCipher()), 372 cipherEntry.getCipher().getMode(), 373 cipherEntry.getCipherTransformation(), 374 cipherEntry.getKeyEntry().getKeyID() 375 } 376 ); 377 378 Map<String, Map<Long, List<CryptoCacheCipherEntry>>> cipherTransformation2keyID2cipherEntries; 379 synchronized (opmode2cipherTransformation2keyID2cipherEntries) { 380 cipherTransformation2keyID2cipherEntries = 381 opmode2cipherTransformation2keyID2cipherEntries.get(cipherEntry.getCipher().getMode()); 382 383 if (cipherTransformation2keyID2cipherEntries == null) { 384 cipherTransformation2keyID2cipherEntries = Collections.synchronizedMap( 385 new HashMap<String, Map<Long,List<CryptoCacheCipherEntry>>>() 386 ); 387 388 opmode2cipherTransformation2keyID2cipherEntries.put( 389 cipherEntry.getCipher().getMode(), cipherTransformation2keyID2cipherEntries 390 ); 391 } 392 } 393 394 Map<Long, List<CryptoCacheCipherEntry>> keyID2cipherEntries; 395 synchronized (cipherTransformation2keyID2cipherEntries) { 396 keyID2cipherEntries = cipherTransformation2keyID2cipherEntries.get(cipherEntry.getCipherTransformation()); 397 if (keyID2cipherEntries == null) { 398 keyID2cipherEntries = Collections.synchronizedMap(new HashMap<Long, List<CryptoCacheCipherEntry>>()); 399 cipherTransformation2keyID2cipherEntries.put(cipherEntry.getCipherTransformation(), keyID2cipherEntries); 400 } 401 } 402 403 List<CryptoCacheCipherEntry> cipherEntries; 404 synchronized (keyID2cipherEntries) { 405 cipherEntries = keyID2cipherEntries.get(cipherEntry.getKeyEntry().getKeyID()); 406 if (cipherEntries == null) { 407 cipherEntries = Collections.synchronizedList(new LinkedList<CryptoCacheCipherEntry>()); 408 keyID2cipherEntries.put(cipherEntry.getKeyEntry().getKeyID(), cipherEntries); 409 } 410 } 411 412 cipherEntries.add(cipherEntry); 413 } 414 415 /** 416 * Clear this cache entirely. This evicts all cached objects - no matter what type. 417 */ 418 public void clear() 419 { 420 logger.trace("clear: entered"); 421 keyID2key.clear(); 422 opmode2cipherTransformation2keyID2cipherEntries.clear(); 423 synchronized (activeEncryptionKeyMutex) { 424 activeEncryptionKeyID = -1; 425 activeEncryptionKeyUntilExcl = null; 426 } 427 } 428 429 private Map<String, CryptoCacheKeyEncryptionKeyEntry> keyEncryptionTransformation2keyEncryptionKey = Collections.synchronizedMap( 430 new HashMap<String, CryptoCacheKeyEncryptionKeyEntry>() 431 ); 432 433 private Map<String, List<CryptoCacheKeyDecrypterEntry>> keyEncryptionTransformation2keyDecryptors = Collections.synchronizedMap( 434 new HashMap<String, List<CryptoCacheKeyDecrypterEntry>>() 435 ); 436 437 /** 438 * How long should the public-private-key-pair for secret-key-encryption be used. After that time, a new 439 * public-private-key-pair is generated. 440 * @return the time a public-private-key-pair should be used. 441 * @see #getKeyEncryptionKey(String) 442 */ 443 protected long getKeyEncryptionKeyActivePeriodMSec() 444 { 445 return 3600L * 1000L * 5L; // use the same key pair for 5 hours - TODO must make this configurable via a persistence property! 446 } 447 448 /** 449 * Get the key-pair that is currently active for secret-key-encryption. 450 * @param keyEncryptionTransformation the transformation to be used for secret-key-encryption. Must not be <code>null</code>. 451 * @return entry wrapping the key-pair that is currently active for secret-key-encryption. 452 */ 453 protected CryptoCacheKeyEncryptionKeyEntry getKeyEncryptionKey(String keyEncryptionTransformation) 454 { 455 if (keyEncryptionTransformation == null) 456 throw new IllegalArgumentException("keyEncryptionTransformation == null"); 457 458 synchronized (keyEncryptionTransformation2keyEncryptionKey) { 459 CryptoCacheKeyEncryptionKeyEntry entry = keyEncryptionTransformation2keyEncryptionKey.get(keyEncryptionTransformation); 460 if (entry != null && !entry.isExpired()) 461 return entry; 462 else 463 entry = null; 464 465 String engineAlgorithmName = CryptoRegistry.splitTransformation(keyEncryptionTransformation)[0]; 466 467 AsymmetricCipherKeyPairGenerator keyPairGenerator; 468 try { 469 keyPairGenerator = CryptoRegistry.sharedInstance().createKeyPairGenerator(engineAlgorithmName, true); 470 } catch (NoSuchAlgorithmException e) { 471 throw new RuntimeException(e); 472 } catch (IllegalArgumentException e) { 473 throw new RuntimeException(e); 474 } 475 476 AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair(); 477 entry = new CryptoCacheKeyEncryptionKeyEntry(keyPair, getKeyEncryptionKeyActivePeriodMSec()); 478 keyEncryptionTransformation2keyEncryptionKey.put(keyEncryptionTransformation, entry); 479 return entry; 480 } 481 } 482 483 /** 484 * Remove the first element from the given list and return it. 485 * If the list is empty, return <code>null</code>. This method is thread-safe, if the given <code>list</code> is. 486 * @param <T> the type of the list's elements. 487 * @param list the list; must not be <code>null</code>. 488 * @return the first element of the list (after removing it) or <code>null</code>, if the list 489 * was empty. 490 */ 491 private static <T> T popOrNull(List<? extends T> list) 492 { 493 try { 494 T element = list.remove(0); 495 return element; 496 } catch (IndexOutOfBoundsException x) { 497 return null; 498 } 499 } 500 501 /** 502 * Acquire a cipher to be used for secret-key-decryption. The cipher is already initialised with the current 503 * {@link #getKeyEncryptionKey(String) keyEncryptionKey} and can thus be directly used. 504 * <p> 505 * You should call {@link #releaseKeyDecryptor(CryptoCacheKeyDecrypterEntry)} to put the cipher back into the cache! 506 * </p> 507 * @param keyEncryptionTransformation the transformation to be used for secret-key-encryption. Must not be <code>null</code>. 508 * @return entry wrapping the cipher that is ready to be used for secret-key-decryption. 509 * @see #releaseKeyDecryptor(CryptoCacheKeyDecrypterEntry) 510 */ 511 public CryptoCacheKeyDecrypterEntry acquireKeyDecryptor(String keyEncryptionTransformation) 512 { 513 if (keyEncryptionTransformation == null) 514 throw new IllegalArgumentException("keyEncryptionTransformation == null"); 515 516 try { 517 List<CryptoCacheKeyDecrypterEntry> decryptors = keyEncryptionTransformation2keyDecryptors.get(keyEncryptionTransformation); 518 if (decryptors != null) { 519 CryptoCacheKeyDecrypterEntry entry; 520 do { 521 entry = popOrNull(decryptors); 522 if (entry != null && !entry.getKeyEncryptionKey().isExpired()) { 523 entry.updateLastUsageTimestamp(); 524 return entry; 525 } 526 } while (entry != null); 527 } 528 529 Cipher keyDecryptor; 530 try { 531 keyDecryptor = CryptoRegistry.sharedInstance().createCipher(keyEncryptionTransformation); 532 } catch (NoSuchAlgorithmException e) { 533 throw new RuntimeException(e); 534 } catch (NoSuchPaddingException e) { 535 throw new RuntimeException(e); 536 } 537 538 CryptoCacheKeyEncryptionKeyEntry keyEncryptionKey = getKeyEncryptionKey(keyEncryptionTransformation); 539 keyDecryptor.init(CipherOperationMode.DECRYPT, keyEncryptionKey.getKeyPair().getPrivate()); 540 CryptoCacheKeyDecrypterEntry entry = new CryptoCacheKeyDecrypterEntry(keyEncryptionKey, keyEncryptionTransformation, keyDecryptor); 541 return entry; 542 } finally { 543 // We do this at the end in order to maybe still fetch an entry that is about to expire just right now. 544 // Otherwise it might happen, that we delete one and recreate it again instead of just reusing it. Marco :-) 545 initTimerTaskOrRemoveExpiredEntriesPeriodically(); 546 } 547 } 548 549 /** 550 * Release a cipher (put it back into the cache). 551 * @param decryptorEntry the entry to be released or <code>null</code> (silently ignored). 552 */ 553 public void releaseKeyDecryptor(CryptoCacheKeyDecrypterEntry decryptorEntry) 554 { 555 if (decryptorEntry == null) 556 return; 557 558 List<CryptoCacheKeyDecrypterEntry> keyDecryptors; 559 synchronized (keyEncryptionTransformation2keyDecryptors) { 560 keyDecryptors = keyEncryptionTransformation2keyDecryptors.get(decryptorEntry.getKeyEncryptionTransformation()); 561 if (keyDecryptors == null) { 562 keyDecryptors = Collections.synchronizedList(new LinkedList<CryptoCacheKeyDecrypterEntry>()); 563 keyEncryptionTransformation2keyDecryptors.put(decryptorEntry.getKeyEncryptionTransformation(), keyDecryptors); 564 } 565 } 566 567 keyDecryptors.add(decryptorEntry); 568 } 569 570 /** 571 * Get a key-pair-generator for the given transformation. 572 * @param keyEncryptionTransformation the transformation (based on an asymmetric crypto algorithm) for which to obtain 573 * a key-pair-generator. 574 * @return the key-pair-generator. 575 */ 576 protected AsymmetricCipherKeyPairGenerator getAsymmetricCipherKeyPairGenerator(String keyEncryptionTransformation) 577 { 578 String algorithmName = CryptoRegistry.splitTransformation(keyEncryptionTransformation)[0]; 579 try { 580 return CryptoRegistry.sharedInstance().createKeyPairGenerator(algorithmName, true); 581 } catch (NoSuchAlgorithmException e) { 582 throw new RuntimeException(e); 583 } 584 } 585 586 587 private static volatile Timer cleanupTimer = null; 588 private static volatile boolean cleanupTimerInitialised = false; 589 private volatile boolean cleanupTaskInitialised = false; 590 591 private static class CleanupTask extends TimerTask 592 { 593 private final Logger logger = LoggerFactory.getLogger(CleanupTask.class); 594 595 private WeakReference<CryptoCache> cryptoCacheRef; 596 private final long expiryTimerPeriodMSec; 597 598 public CleanupTask(CryptoCache cryptoCache, long expiryTimerPeriodMSec) 599 { 600 if (cryptoCache == null) 601 throw new IllegalArgumentException("cryptoCache == null"); 602 603 this.cryptoCacheRef = new WeakReference<CryptoCache>(cryptoCache); 604 this.expiryTimerPeriodMSec = expiryTimerPeriodMSec; 605 } 606 607 @Override 608 public void run() { 609 try { 610 logger.debug("run: entered"); 611 final CryptoCache cryptoCache = cryptoCacheRef.get(); 612 if (cryptoCache == null) { 613 logger.info("run: CryptoCache was garbage-collected. Cancelling this TimerTask."); 614 this.cancel(); 615 return; 616 } 617 618 cryptoCache.removeExpiredEntries(true); 619 620 long currentPeriodMSec = cryptoCache.getCleanupTimerPeriod(); 621 if (currentPeriodMSec != expiryTimerPeriodMSec) { 622 logger.info( 623 "run: The expiryTimerPeriodMSec changed (oldValue={}, newValue={}). Re-scheduling this task.", 624 expiryTimerPeriodMSec, currentPeriodMSec 625 ); 626 this.cancel(); 627 628 cleanupTimer.schedule(new CleanupTask(cryptoCache, currentPeriodMSec), currentPeriodMSec, currentPeriodMSec); 629 } 630 } catch (Throwable x) { 631 // The TimerThread is cancelled, if a task throws an exception. Furthermore, they are not logged at all. 632 // Since we do not want the TimerThread to die, we catch everything (Throwable - not only Exception) and log 633 // it here. IMHO there's nothing better we can do. Marco :-) 634 logger.error("run: " + x, x); 635 } 636 } 637 }; 638 639 private final void initTimerTaskOrRemoveExpiredEntriesPeriodically() 640 { 641 if (!cleanupTimerInitialised) { 642 synchronized (AbstractCryptoManager.class) { 643 if (!cleanupTimerInitialised) { 644 if (getCleanupTimerEnabled()) 645 cleanupTimer = new Timer(CryptoCache.class.getSimpleName(), true); 646 647 cleanupTimerInitialised = true; 648 } 649 } 650 } 651 652 if (!cleanupTaskInitialised) { 653 synchronized (this) { 654 if (!cleanupTaskInitialised) { 655 if (cleanupTimer != null) { 656 long periodMSec = getCleanupTimerPeriod(); 657 cleanupTimer.schedule(new CleanupTask(this, periodMSec), periodMSec, periodMSec); 658 } 659 cleanupTaskInitialised = true; 660 } 661 } 662 } 663 664 if (cleanupTimer == null) { 665 logger.trace("initTimerTaskOrRemoveExpiredEntriesPeriodically: No timer enabled => calling removeExpiredEntries(false) now."); 666 removeExpiredEntries(false); 667 } 668 } 669 670 private Date lastRemoveExpiredEntriesTimestamp = null; 671 672 private void removeExpiredEntries(boolean force) 673 { 674 synchronized (this) { 675 if ( 676 !force && ( 677 lastRemoveExpiredEntriesTimestamp != null && 678 lastRemoveExpiredEntriesTimestamp.after(new Date(System.currentTimeMillis() - getCleanupTimerPeriod())) 679 ) 680 ) 681 { 682 logger.trace("removeExpiredEntries: force == false and period not yet elapsed. Skipping."); 683 return; 684 } 685 686 lastRemoveExpiredEntriesTimestamp = new Date(); 687 } 688 689 Date removeEntriesBeforeThisTimestamp = new Date( 690 System.currentTimeMillis() - getCryptoCacheEntryExpiryAge() 691 ); 692 693 int totalEntryCounter = 0; 694 int removedEntryCounter = 0; 695 synchronized (keyEncryptionTransformation2keyEncryptionKey) { 696 for (Iterator<Map.Entry<String, CryptoCacheKeyEncryptionKeyEntry>> it1 = keyEncryptionTransformation2keyEncryptionKey.entrySet().iterator(); it1.hasNext(); ) { 697 Map.Entry<String, CryptoCacheKeyEncryptionKeyEntry> me1 = it1.next(); 698 if (me1.getValue().isExpired()) { 699 it1.remove(); 700 ++removedEntryCounter; 701 } 702 else 703 ++totalEntryCounter; 704 } 705 } 706 logger.debug("removeExpiredEntries: Removed {} instances of CryptoCacheKeyEncryptionKeyEntry ({} left).", removedEntryCounter, totalEntryCounter); 707 708 709 // There are not many keyEncryptionTransformations (usually only ONE!), hence copying this is fine and very fast. 710 String[] keyEncryptionTransformations; 711 synchronized (keyEncryptionTransformation2keyDecryptors) { 712 keyEncryptionTransformations = keyEncryptionTransformation2keyDecryptors.keySet().toArray( 713 new String[keyEncryptionTransformation2keyDecryptors.size()] 714 ); 715 } 716 717 totalEntryCounter = 0; 718 removedEntryCounter = 0; 719 for (String keyEncryptionTransformation : keyEncryptionTransformations) { 720 List<CryptoCacheKeyDecrypterEntry> entries = keyEncryptionTransformation2keyDecryptors.get(keyEncryptionTransformation); 721 if (entries == null) // should never happen, but better check :-) 722 continue; 723 724 synchronized (entries) { 725 for (Iterator<CryptoCacheKeyDecrypterEntry> itEntry = entries.iterator(); itEntry.hasNext(); ) { 726 CryptoCacheKeyDecrypterEntry entry = itEntry.next(); 727 if (entry.getLastUsageTimestamp().before(removeEntriesBeforeThisTimestamp) || entry.getKeyEncryptionKey().isExpired()) { 728 itEntry.remove(); 729 ++removedEntryCounter; 730 } 731 else 732 ++totalEntryCounter; 733 } 734 } 735 } 736 logger.debug("removeExpiredEntries: Removed {} instances of CryptoCacheKeyDecrypterEntry ({} left).", removedEntryCounter, totalEntryCounter); 737 738 739 totalEntryCounter = 0; 740 removedEntryCounter = 0; 741 synchronized (keyID2key) { 742 for (Iterator<Map.Entry<Long, CryptoCacheKeyEntry>> it1 = keyID2key.entrySet().iterator(); it1.hasNext(); ) { 743 Map.Entry<Long, CryptoCacheKeyEntry> me1 = it1.next(); 744 if (me1.getValue().getLastUsageTimestamp().before(removeEntriesBeforeThisTimestamp)) { 745 it1.remove(); 746 ++removedEntryCounter; 747 } 748 else 749 ++totalEntryCounter; 750 } 751 } 752 logger.debug("removeExpiredEntries: Removed {} instances of CryptoCacheKeyEntry ({} left).", removedEntryCounter, totalEntryCounter); 753 754 755 totalEntryCounter = 0; 756 removedEntryCounter = 0; 757 int totalListCounter = 0; 758 int removedListCounter = 0; 759 for (CipherOperationMode opmode : CipherOperationMode.values()) { 760 Map<String, Map<Long, List<CryptoCacheCipherEntry>>> encryptionAlgorithm2keyID2cipherEntries = opmode2cipherTransformation2keyID2cipherEntries.get(opmode); 761 if (encryptionAlgorithm2keyID2cipherEntries == null) 762 continue; 763 764 // There are not many encryptionAlgorithms (usually only ONE!), hence copying this is fine and very fast. 765 String[] encryptionAlgorithms; 766 synchronized (encryptionAlgorithm2keyID2cipherEntries) { 767 encryptionAlgorithms = encryptionAlgorithm2keyID2cipherEntries.keySet().toArray( 768 new String[encryptionAlgorithm2keyID2cipherEntries.size()] 769 ); 770 } 771 772 for (String encryptionAlgorithm : encryptionAlgorithms) { 773 Map<Long, List<CryptoCacheCipherEntry>> keyID2cipherEntries = encryptionAlgorithm2keyID2cipherEntries.get(encryptionAlgorithm); 774 if (keyID2cipherEntries == null) // should never happen, but well, better check ;-) 775 continue; 776 777 synchronized (keyID2cipherEntries) { 778 for (Iterator<Map.Entry<Long, List<CryptoCacheCipherEntry>>> it1 = keyID2cipherEntries.entrySet().iterator(); it1.hasNext(); ) { 779 Map.Entry<Long, List<CryptoCacheCipherEntry>> me1 = it1.next(); 780 List<CryptoCacheCipherEntry> entries = me1.getValue(); 781 synchronized (entries) { 782 for (Iterator<CryptoCacheCipherEntry> it2 = entries.iterator(); it2.hasNext(); ) { 783 CryptoCacheCipherEntry entry = it2.next(); 784 if (entry.getLastUsageTimestamp().before(removeEntriesBeforeThisTimestamp)) { 785 it2.remove(); 786 ++removedEntryCounter; 787 } 788 else 789 ++totalEntryCounter; 790 } 791 792 if (entries.isEmpty()) { 793 it1.remove(); 794 ++removedListCounter; 795 } 796 else 797 ++totalListCounter; 798 } 799 } 800 } 801 } 802 } 803 logger.debug("removeExpiredEntries: Removed {} instances of CryptoCacheCipherEntry ({} left).", removedEntryCounter, totalEntryCounter); 804 logger.debug("removeExpiredEntries: Removed {} instances of empty List<CryptoCacheCipherEntry> ({} non-empty lists left).", removedListCounter, totalListCounter); 805 } 806 807 /** 808 * <p> 809 * Persistence property to control when the timer for cleaning up expired {@link CryptoCache}-entries is called. The 810 * value configured here is a period in milliseconds, i.e. the timer will be triggered every X ms (roughly). 811 * </p><p> 812 * If this persistence property is not present (or not a valid number), the default is 60000 (1 minute), which means 813 * the timer will wake up once a minute and call {@link #removeExpiredEntries(boolean)} with <code>force = true</code>. 814 * </p> 815 */ 816 public static final String PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD = "cumulus4j.CryptoCache.cleanupTimer.period"; 817 818 /** 819 * <p> 820 * Persistence property to control whether the timer for cleaning up expired {@link CryptoCache}-entries is enabled. The 821 * value configured here can be either <code>true</code> or <code>false</code>. 822 * </p><p> 823 * If this persistence property is not present (or not a valid number), the default is <code>true</code>, which means the 824 * timer is enabled and will periodically call {@link #removeExpiredEntries(boolean)} with <code>force = true</code>. 825 * </p><p> 826 * If this persistence property is set to <code>false</code>, the timer is deactivated and cleanup happens only synchronously 827 * when one of the release-methods is called; periodically - not every time a method is called. The period is in this 828 * case the same as for the timer, i.e. configurable via {@link #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD}. 829 * </p> 830 */ 831 public static final String PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_ENABLED = "cumulus4j.CryptoCache.cleanupTimer.enabled"; 832 833 private long cleanupTimerPeriod = Long.MIN_VALUE; 834 835 private Boolean cleanupTimerEnabled = null; 836 837 /** 838 * <p> 839 * Persistence property to control after which time an unused entry expires. 840 * </p><p> 841 * Entries that are unused for the configured time in milliseconds are considered expired and 842 * either periodically removed by a timer (see property {@value #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD}) 843 * or periodically removed synchronously during a call to one of the release-methods. 844 * </p><p> 845 * If this property is not present (or not a valid number), the default value is 1800000 (30 minutes). 846 * </p> 847 */ 848 public static final String PROPERTY_CRYPTO_CACHE_ENTRY_EXPIRY_AGE = "cumulus4j.CryptoCache.entryExpiryAge"; 849 850 private long cryptoCacheEntryExpiryAge = Long.MIN_VALUE; 851 852 /** 853 * <p> 854 * Get the period in which expired entries are searched and closed. 855 * </p> 856 * <p> 857 * This value can be configured using the persistence property {@value #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD}. 858 * </p> 859 * 860 * @return the period in milliseconds. 861 * @see #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD 862 * @see #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_ENABLED 863 */ 864 protected long getCleanupTimerPeriod() 865 { 866 long val = cleanupTimerPeriod; 867 if (val == Long.MIN_VALUE) { 868 String propName = PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD; 869 String propVal = (String) cryptoManager.getCryptoManagerRegistry().getNucleusContext().getPersistenceConfiguration().getProperty(propName); 870 propVal = propVal == null ? null : propVal.trim(); 871 if (propVal != null && !propVal.isEmpty()) { 872 try { 873 val = Long.parseLong(propVal); 874 if (val <= 0) { 875 logger.warn("Persistence property '{}' is set to '{}', which is an ILLEGAL value (<= 0). Falling back to default value.", propName, propVal); 876 val = Long.MIN_VALUE; 877 } 878 else 879 logger.info("Persistence property '{}' is set to {} ms.", propName, val); 880 } catch (NumberFormatException x) { 881 logger.warn("Persistence property '{}' is set to '{}', which is an ILLEGAL value (no valid number). Falling back to default value.", propName, propVal); 882 } 883 } 884 885 if (val == Long.MIN_VALUE) { 886 val = 60000L; 887 logger.info("Persistence property '{}' is not set. Using default value {}.", propName, val); 888 } 889 890 cleanupTimerPeriod = val; 891 } 892 return val; 893 } 894 895 /** 896 * <p> 897 * Get the enabled status of the timer used to cleanup. 898 * </p> 899 * <p> 900 * This value can be configured using the persistence property {@value #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_ENABLED}. 901 * </p> 902 * 903 * @return the enabled status. 904 * @see #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_PERIOD 905 * @see #PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_ENABLED 906 */ 907 protected boolean getCleanupTimerEnabled() 908 { 909 Boolean val = cleanupTimerEnabled; 910 if (val == null) { 911 String propName = PROPERTY_CRYPTO_CACHE_CLEANUP_TIMER_ENABLED; 912 String propVal = (String) cryptoManager.getCryptoManagerRegistry().getNucleusContext().getPersistenceConfiguration().getProperty(propName); 913 propVal = propVal == null ? null : propVal.trim(); 914 if (propVal != null && !propVal.isEmpty()) { 915 if (propVal.equalsIgnoreCase(Boolean.TRUE.toString())) 916 val = Boolean.TRUE; 917 else if (propVal.equalsIgnoreCase(Boolean.FALSE.toString())) 918 val = Boolean.FALSE; 919 920 if (val == null) 921 logger.warn("getCryptoCacheCleanupTimerEnabled: Property '{}' is set to '{}', which is an ILLEGAL value. Falling back to default value.", propName, propVal); 922 else 923 logger.info("getCryptoCacheCleanupTimerEnabled: Property '{}' is set to '{}'.", propName, val); 924 } 925 926 if (val == null) { 927 val = Boolean.TRUE; 928 logger.info("getCryptoCacheCleanupTimerEnabled: Property '{}' is not set. Using default value {}.", propName, val); 929 } 930 931 cleanupTimerEnabled = val; 932 } 933 return val; 934 } 935 936 /** 937 * <p> 938 * Get the age after which an unused entry expires. 939 * </p> 940 * <p> 941 * An entry expires when its lastUsageTimestamp 942 * is longer in the past than this expiry age. Note, that the entry might be kept longer, because a 943 * timer checks {@link #getCryptoCacheEntryExpiryTimerPeriod() periodically} for expired entries. 944 * </p> 945 * 946 * @return the expiry age (of non-usage-time) in milliseconds, after which an entry should be expired (and thus removed). 947 */ 948 protected long getCryptoCacheEntryExpiryAge() 949 { 950 long val = cryptoCacheEntryExpiryAge; 951 if (val == Long.MIN_VALUE) { 952 String propName = PROPERTY_CRYPTO_CACHE_ENTRY_EXPIRY_AGE; 953 954 CryptoManagerRegistry cryptoManagerRegistry = cryptoManager.getCryptoManagerRegistry(); 955 if (cryptoManagerRegistry == null) 956 throw new IllegalStateException("cryptoManager.getCryptoManagerRegistry() returned null!"); 957 958 NucleusContext nucleusContext = cryptoManagerRegistry.getNucleusContext(); 959 if (nucleusContext == null) { 960 // throw new IllegalStateException("cryptoManagerRegistry.getNucleusContext() returned null!"); 961 // garbage-collected => close quickly => return small value 962 val = 5L * 60000L; 963 logger.info("getCryptoCacheEntryExpiryAgeMSec: Property '{}' cannot be read, because NucleusContext was garbage-collected. Using fallback value {}.", propName, val); 964 } 965 else { 966 PersistenceConfiguration persistenceConfiguration = nucleusContext.getPersistenceConfiguration(); 967 if (persistenceConfiguration == null) 968 throw new IllegalStateException("nucleusContext.getPersistenceConfiguration() returned null!"); 969 970 String propVal = (String) persistenceConfiguration.getProperty(propName); 971 // TO DO Fix NPE! Just had a NullPointerException in the above line: 972 // 22:48:39,028 ERROR [Timer-3][CryptoCache$CleanupTask] run: java.lang.NullPointerException 973 // java.lang.NullPointerException 974 // at org.cumulus4j.store.crypto.keymanager.CryptoCache.getCryptoCacheEntryExpiryAge(CryptoCache.java:950) 975 // at org.cumulus4j.store.crypto.keymanager.CryptoCache.removeExpiredEntries(CryptoCache.java:686) 976 // at org.cumulus4j.store.crypto.keymanager.CryptoCache.access$000(CryptoCache.java:56) 977 // at org.cumulus4j.store.crypto.keymanager.CryptoCache$CleanupTask.run(CryptoCache.java:615) 978 // at java.util.TimerThread.mainLoop(Timer.java:512) 979 // at java.util.TimerThread.run(Timer.java:462) 980 // Need to check what exactly is null and if that is allowed or there is another problem. 981 // Update 2012-11-11: NPE above should be fixed now. Marco :-) 982 propVal = propVal == null ? null : propVal.trim(); 983 if (propVal != null && !propVal.isEmpty()) { 984 try { 985 val = Long.parseLong(propVal); 986 logger.info("getCryptoCacheEntryExpiryAgeMSec: Property '{}' is set to {} ms.", propName, val); 987 } catch (NumberFormatException x) { 988 logger.warn("getCryptoCacheEntryExpiryAgeMSec: Property '{}' is set to '{}', which is an ILLEGAL value (no valid number). Falling back to default value.", propName, propVal); 989 } 990 } 991 992 if (val == Long.MIN_VALUE) { 993 val = 30L * 60000L; 994 logger.info("getCryptoCacheEntryExpiryAgeMSec: Property '{}' is not set. Using default value {}.", propName, val); 995 } 996 } 997 998 cryptoCacheEntryExpiryAge = val; 999 } 1000 return val; 1001 } 1002 }