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 }