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