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 &lt;= 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    }