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