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 }