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;
019    
020    import java.lang.ref.WeakReference;
021    import java.util.Date;
022    import java.util.HashMap;
023    import java.util.Locale;
024    import java.util.Map;
025    import java.util.Timer;
026    import java.util.TimerTask;
027    
028    import org.datanucleus.NucleusContext;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    /**
033     * <p>
034     * Abstract base-class for implementing {@link CryptoManager}s.
035     * </p>
036     * <p>
037     * This class already implements a mechanism to close expired {@link CryptoSession}s
038     * periodically (see {@link #getCryptoSessionExpiryAge()} and {@link #getCryptoSessionExpiryTimerPeriod()}).
039     * </p>
040     *
041     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
042     */
043    public abstract class AbstractCryptoManager implements CryptoManager
044    {
045            private static final Logger logger = LoggerFactory.getLogger(AbstractCryptoManager.class);
046    
047            private CryptoManagerRegistry cryptoManagerRegistry;
048    
049            private String cryptoManagerID;
050    
051            private Map<String, CryptoSession> id2session = new HashMap<String, CryptoSession>();
052    
053            private static volatile Timer closeExpiredSessionsTimer = null;
054            private static volatile boolean closeExpiredSessionsTimerInitialised = false;
055            private volatile boolean closeExpiredSessionsTaskInitialised = false;
056    
057            private static class CloseExpiredSessionsTask extends TimerTask
058            {
059                    private final Logger logger = LoggerFactory.getLogger(CloseExpiredSessionsTask.class);
060    
061                    private WeakReference<AbstractCryptoManager> abstractCryptoManagerRef;
062                    private final long expiryTimerPeriodMSec;
063    
064                    public CloseExpiredSessionsTask(AbstractCryptoManager abstractCryptoManager, long expiryTimerPeriodMSec)
065                    {
066                            if (abstractCryptoManager == null)
067                                    throw new IllegalArgumentException("abstractCryptoManager == null");
068    
069                            this.abstractCryptoManagerRef = new WeakReference<AbstractCryptoManager>(abstractCryptoManager);
070                            this.expiryTimerPeriodMSec = expiryTimerPeriodMSec;
071                    }
072    
073                    @Override
074                    public void run() {
075                            try {
076                                    logger.debug("run: entered");
077                                    final AbstractCryptoManager abstractCryptoManager = abstractCryptoManagerRef.get();
078                                    if (abstractCryptoManager == null) {
079                                            logger.info("run: AbstractCryptoManager was garbage-collected. Cancelling this TimerTask.");
080                                            this.cancel();
081                                            return;
082                                    }
083    
084                                    abstractCryptoManager.closeExpiredCryptoSessions(true);
085    
086                                    long currentPeriodMSec = abstractCryptoManager.getCryptoSessionExpiryTimerPeriod();
087                                    if (currentPeriodMSec != expiryTimerPeriodMSec) {
088                                            logger.info(
089                                                            "run: The expiryTimerPeriodMSec changed (oldValue={}, newValue={}). Re-scheduling this task.",
090                                                            expiryTimerPeriodMSec, currentPeriodMSec
091                                            );
092                                            this.cancel();
093    
094                                            closeExpiredSessionsTimer.schedule(new CloseExpiredSessionsTask(abstractCryptoManager, currentPeriodMSec), currentPeriodMSec, currentPeriodMSec);
095                                    }
096                            } catch (Throwable x) {
097                                    // The TimerThread is cancelled, if a task throws an exception. Furthermore, they are not logged at all.
098                                    // Since we do not want the TimerThread to die, we catch everything (Throwable - not only Exception) and log
099                                    // it here. IMHO there's nothing better we can do. Marco :-)
100                                    logger.error("run: " + x, x);
101                            }
102                    }
103            };
104    
105            private long cryptoSessionExpiryTimerPeriod = Long.MIN_VALUE;
106    
107            private Boolean cryptoSessionExpiryTimerEnabled = null;
108    
109            private long cryptoSessionExpiryAge = Long.MIN_VALUE;
110    
111            /**
112             * <p>
113             * Get the period in which expired crypto sessions are searched and closed.
114             * </p>
115             * <p>
116             * This value can be configured using the persistence property {@value CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_PERIOD}.
117             * </p>
118             *
119             * @return the period in milliseconds.
120             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_PERIOD
121             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_ENABLED
122             */
123            protected long getCryptoSessionExpiryTimerPeriod()
124            {
125                    long val = cryptoSessionExpiryTimerPeriod;
126                    if (val == Long.MIN_VALUE) {
127                            String propName = PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_PERIOD;
128                            String propVal = (String) getCryptoManagerRegistry().getNucleusContext().getPersistenceConfiguration().getProperty(propName);
129                            propVal = propVal == null ? null : propVal.trim();
130                            if (propVal != null && !propVal.isEmpty()) {
131                                    try {
132                                            val = Long.parseLong(propVal);
133                                            if (val <= 0) {
134                                                    logger.warn("getCryptoSessionExpiryTimerPeriod: Property '{}' is set to '{}', which is an ILLEGAL value (<= 0). Falling back to default value.", propName, propVal);
135                                                    val = Long.MIN_VALUE;
136                                            }
137                                            else
138                                                    logger.info("getCryptoSessionExpiryTimerPeriod: Property '{}' is set to {} ms.", propName, val);
139                                    } catch (NumberFormatException x) {
140                                            logger.warn("getCryptoSessionExpiryTimerPeriod: Property '{}' is set to '{}', which is an ILLEGAL value (no valid number). Falling back to default value.", propName, propVal);
141                                    }
142                            }
143    
144                            if (val == Long.MIN_VALUE) {
145                                    val = 60000L;
146                                    logger.info("getCryptoSessionExpiryTimerPeriod: Property '{}' is not set. Using default value {}.", propName, val);
147                            }
148    
149                            cryptoSessionExpiryTimerPeriod = val;
150                    }
151                    return val;
152            }
153    
154            /**
155             * <p>
156             * Get the enabled status of the timer used to cleanup.
157             * </p>
158             * <p>
159             * This value can be configured using the persistence property {@value CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_ENABLED}.
160             * </p>
161             *
162             * @return the enabled status.
163             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_ENABLED
164             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_PERIOD
165             */
166            protected boolean getCryptoSessionExpiryTimerEnabled()
167            {
168                    Boolean val = cryptoSessionExpiryTimerEnabled;
169                    if (val == null) {
170                            String propName = PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_ENABLED;
171                            String propVal = (String) getCryptoManagerRegistry().getNucleusContext().getPersistenceConfiguration().getProperty(propName);
172                            propVal = propVal == null ? null : propVal.trim();
173                            if (propVal != null && !propVal.isEmpty()) {
174                                    if (propVal.equalsIgnoreCase(Boolean.TRUE.toString()))
175                                            val = Boolean.TRUE;
176                                    else if (propVal.equalsIgnoreCase(Boolean.FALSE.toString()))
177                                            val = Boolean.FALSE;
178    
179                                    if (val == null)
180                                            logger.warn("getCryptoSessionExpiryTimerEnabled: Property '{}' is set to '{}', which is an ILLEGAL value. Falling back to default value.", propName, propVal);
181                                    else
182                                            logger.info("getCryptoSessionExpiryTimerEnabled: Property '{}' is set to '{}'.", propName, val);
183                            }
184    
185                            if (val == null) {
186                                    val = Boolean.TRUE;
187                                    logger.info("getCryptoSessionExpiryTimerEnabled: Property '{}' is not set. Using default value {}.", propName, val);
188                            }
189    
190                            cryptoSessionExpiryTimerEnabled = val;
191                    }
192                    return val;
193            }
194    
195            /**
196             * <p>
197             * Get the age after which an unused session expires.
198             * </p><p>
199             * This value can be configured using the persistence property {@value CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_AGE}.
200             * </p><p>
201             * A {@link CryptoSession} expires when its {@link CryptoSession#getLastUsageTimestamp() lastUsageTimestamp}
202             * is longer in the past than this expiry age. Note, that the session might be kept longer, because a
203             * timer checks {@link #getCryptoSessionExpiryTimerPeriod() periodically} for expired sessions.
204             * </p>
205             *
206             * @return the expiry age (of non-usage-time) in milliseconds, after which the session should be closed.
207             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_AGE
208             */
209            protected long getCryptoSessionExpiryAge()
210            {
211                    long val = cryptoSessionExpiryAge;
212                    if (val == Long.MIN_VALUE) {
213                            String propName = PROPERTY_CRYPTO_SESSION_EXPIRY_AGE;
214                            String propVal = (String) getCryptoManagerRegistry().getNucleusContext().getPersistenceConfiguration().getProperty(propName);
215                            // TODO Check whether this is a potential NPE! Just had another NullPointerException but similar to the above line:
216    //                      22:48:39,028 ERROR [Timer-3][CryptoCache$CleanupTask] run: java.lang.NullPointerException
217    //                      java.lang.NullPointerException
218    //                              at org.cumulus4j.store.crypto.keymanager.CryptoCache.getCryptoCacheEntryExpiryAge(CryptoCache.java:950)
219    //                              at org.cumulus4j.store.crypto.keymanager.CryptoCache.removeExpiredEntries(CryptoCache.java:686)
220    //                              at org.cumulus4j.store.crypto.keymanager.CryptoCache.access$000(CryptoCache.java:56)
221    //                              at org.cumulus4j.store.crypto.keymanager.CryptoCache$CleanupTask.run(CryptoCache.java:615)
222    //                              at java.util.TimerThread.mainLoop(Timer.java:512)
223    //                              at java.util.TimerThread.run(Timer.java:462)
224    
225                            propVal = propVal == null ? null : propVal.trim();
226                            if (propVal != null && !propVal.isEmpty()) {
227                                    try {
228                                            val = Long.parseLong(propVal);
229                                            if (val <= 0) {
230                                                    logger.warn("getCryptoSessionExpiryAgeMSec: Property '{}' is set to '{}', which is an ILLEGAL value (<= 0). Falling back to default value.", propName, propVal);
231                                                    val = Long.MIN_VALUE;
232                                            }
233                                            else
234                                                    logger.info("getCryptoSessionExpiryAgeMSec: Property '{}' is set to {} ms.", propName, val);
235                                    } catch (NumberFormatException x) {
236                                            logger.warn("getCryptoSessionExpiryAgeMSec: Property '{}' is set to '{}', which is an ILLEGAL value (no valid number). Falling back to default value.", propName, propVal);
237                                    }
238                            }
239    
240                            if (val == Long.MIN_VALUE) {
241                                    val =  30L * 60000L;
242                                    logger.info("getCryptoSessionExpiryAgeMSec: Property '{}' is not set. Using default value {}.", propName, val);
243                            }
244    
245                            cryptoSessionExpiryAge = val;
246                    }
247                    return val;
248            }
249    
250            private Date lastCloseExpiredCryptoSessionsTimestamp = null;
251    
252            /**
253             * <p>
254             * Close expired {@link CryptoSession}s. If <code>force == false</code>, it does so only periodically.
255             * </p><p>
256             * This method is called by {@link #getCryptoSession(String)} with <code>force == false</code>, if the timer
257             * is disabled {@link #getCryptoSessionExpiryTimerPeriod() timer-period == 0}. If the timer is enabled,
258             * it is called periodically by the timer with <code>force == true</code>.
259             * </p><p>
260             * </p>
261             *
262             * @param force whether to force the cleanup now or only do it periodically.
263             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_AGE
264             * @see CryptoManager#PROPERTY_CRYPTO_SESSION_EXPIRY_TIMER_PERIOD
265             */
266            protected void closeExpiredCryptoSessions(boolean force)
267            {
268                    synchronized (this) {
269                            if (
270                                            !force && (
271                                                            lastCloseExpiredCryptoSessionsTimestamp != null &&
272                                                            lastCloseExpiredCryptoSessionsTimestamp.after(new Date(System.currentTimeMillis() - getCryptoSessionExpiryTimerPeriod()))
273                                            )
274                            )
275                            {
276                                    logger.trace("closeExpiredCryptoSessions: force == false and period not yet elapsed. Skipping.");
277                                    return;
278                            }
279    
280                            lastCloseExpiredCryptoSessionsTimestamp = new Date();
281                    }
282    
283                    Date closeSessionsBeforeThisTimestamp = new Date(
284                                    System.currentTimeMillis() - getCryptoSessionExpiryAge()
285                                    - 60000L // additional buffer, preventing the implicit closing here and the getCryptoSession(...) method getting into a collision
286                    );
287    
288                    CryptoSession[] sessions;
289                    synchronized (id2session) {
290                            sessions = id2session.values().toArray(new CryptoSession[id2session.size()]);
291                    }
292    
293                    for (CryptoSession session : sessions) {
294                            if (session.getLastUsageTimestamp().before(closeSessionsBeforeThisTimestamp)) {
295                                    logger.debug("closeExpiredCryptoSessions: Closing expired session: " + session);
296                                    session.close();
297                            }
298                    }
299            }
300    
301    
302            @Override
303            public CryptoManagerRegistry getCryptoManagerRegistry() {
304                    return cryptoManagerRegistry;
305            }
306    
307            @Override
308            public void setCryptoManagerRegistry(CryptoManagerRegistry cryptoManagerRegistry) {
309                    this.cryptoManagerRegistry = cryptoManagerRegistry;
310            }
311    
312            @Override
313            public String getCryptoManagerID() {
314                    return cryptoManagerID;
315            }
316    
317            @Override
318            public void setCryptoManagerID(String cryptoManagerID)
319            {
320                    if (cryptoManagerID == null)
321                            throw new IllegalArgumentException("cryptoManagerID == null");
322    
323                    if (cryptoManagerID.equals(this.cryptoManagerID))
324                            return;
325    
326                    if (this.cryptoManagerID != null)
327                            throw new IllegalStateException("this.keyManagerID is already assigned and cannot be modified!");
328    
329                    this.cryptoManagerID = cryptoManagerID;
330            }
331    
332            /**
333             * <p>
334             * Create a new instance of a class implementing {@link CryptoSession}.
335             * </p>
336             * <p>
337             * This method is called by {@link #getCryptoSession(String)}, if it needs a new <code>CryptoSession</code> instance.
338             * </p>
339             * <p>
340             * Implementors should simply instantiate and return their implementation of
341             * <code>CryptoSession</code>. It is not necessary to call {@link CryptoSession#setCryptoSessionID(String)}
342             * and the like here - this is automatically done afterwards by {@link #getCryptoSession(String)}.
343             * </p>
344             *
345             * @return the new {@link CryptoSession} instance.
346             */
347            protected abstract CryptoSession createCryptoSession();
348    
349            private final void initTimerTask()
350            {
351                    if (!closeExpiredSessionsTimerInitialised) {
352                            synchronized (AbstractCryptoManager.class) {
353                                    if (!closeExpiredSessionsTimerInitialised) {
354                                            if (getCryptoSessionExpiryTimerEnabled())
355                                                    closeExpiredSessionsTimer = new Timer(AbstractCryptoManager.class.getSimpleName(), true);
356    
357                                            closeExpiredSessionsTimerInitialised = true;
358                                    }
359                            }
360                    }
361    
362                    if (!closeExpiredSessionsTaskInitialised) {
363                            synchronized (this) {
364                                    if (!closeExpiredSessionsTaskInitialised) {
365                                            if (closeExpiredSessionsTimer != null) {
366                                                    long periodMSec = getCryptoSessionExpiryTimerPeriod();
367                                                    closeExpiredSessionsTimer.schedule(new CloseExpiredSessionsTask(this, periodMSec), periodMSec, periodMSec);
368                                            }
369                                            closeExpiredSessionsTaskInitialised = true;
370                                    }
371                            }
372                    }
373            }
374    
375            @Override
376            public CryptoSession getCryptoSession(String cryptoSessionID)
377            {
378                    initTimerTask();
379    
380                    CryptoSession session = null;
381                    do {
382                            synchronized (id2session) {
383                                    session = id2session.get(cryptoSessionID);
384                                    if (session == null) {
385                                            session = createCryptoSession();
386                                            if (session == null)
387                                                    throw new IllegalStateException("Implementation error! " + this.getClass().getName() + ".createSession() returned null!");
388    
389                                            session.setCryptoManager(this);
390                                            session.setCryptoSessionID(cryptoSessionID);
391    
392                                            id2session.put(cryptoSessionID, session);
393                                    }
394                            }
395    
396                            // The following code tries to prevent the situation that a CryptoSession is returned which is right
397                            // now simultaneously being closed by the CloseExpiredSessionsTask (the timer above).
398                            Date sessionExpiredBeforeThisTimestamp = new Date(System.currentTimeMillis() - getCryptoSessionExpiryAge());
399                            if (session.getLastUsageTimestamp().before(sessionExpiredBeforeThisTimestamp)) {
400                                    logger.info("getCryptoSession: CryptoSession cryptoSessionID=\"{}\" already expired. Closing it now and repeating lookup.", cryptoSessionID);
401    
402                                    // cause creation of a new session
403                                    session.close();
404                                    session = null;
405                            }
406    
407                    } while (session == null);
408    
409                    session.updateLastUsageTimestamp();
410    
411                    if (closeExpiredSessionsTimer == null) {
412                            logger.trace("getCryptoSession: No timer enabled => calling closeExpiredCryptoSessions(false) now.");
413                            closeExpiredCryptoSessions(false);
414                    }
415    
416                    return session;
417            }
418    
419            @Override
420            public void onCloseCryptoSession(CryptoSession cryptoSession)
421            {
422                    synchronized (id2session) {
423                            id2session.remove(cryptoSession.getCryptoSessionID());
424                    }
425            }
426    
427            @Override
428            public String getEncryptionAlgorithm()
429            {
430                    String ea = encryptionAlgorithm;
431    
432                    if (ea == null) {
433                            NucleusContext nucleusContext = getCryptoManagerRegistry().getNucleusContext();
434                            if (nucleusContext == null)
435                                    throw new IllegalStateException("NucleusContext already garbage-collected!");
436    
437                            String encryptionAlgorithmPropName = PROPERTY_ENCRYPTION_ALGORITHM;
438                            String encryptionAlgorithmPropValue = (String) nucleusContext.getPersistenceConfiguration().getProperty(encryptionAlgorithmPropName);
439                            if (encryptionAlgorithmPropValue == null || encryptionAlgorithmPropValue.trim().isEmpty()) {
440                                    ea = "Twofish/GCM/NoPadding"; // default value, if the property was not defined.
441    //                              ea = "Twofish/CBC/PKCS5Padding"; // default value, if the property was not defined.
442    //                              ea = "AES/CBC/PKCS5Padding"; // default value, if the property was not defined.
443    //                              ea = "AES/CFB/NoPadding"; // default value, if the property was not defined.
444                                    logger.info("getEncryptionAlgorithm: Property '{}' is not set. Using default algorithm '{}'.", encryptionAlgorithmPropName, ea);
445                            }
446                            else {
447                                    ea = encryptionAlgorithmPropValue.trim();
448                                    logger.info("getEncryptionAlgorithm: Property '{}' is set to '{}'. Using this encryption algorithm.", encryptionAlgorithmPropName, ea);
449                            }
450                            ea = ea.toUpperCase(Locale.ENGLISH);
451                            encryptionAlgorithm = ea;
452                    }
453    
454                    return ea;
455            }
456            private String encryptionAlgorithm = null;
457    
458            @Override
459            public String getMACAlgorithm()
460            {
461                    String ma = macAlgorithm;
462    
463                    if (ma == null) {
464                            NucleusContext nucleusContext = getCryptoManagerRegistry().getNucleusContext();
465                            if (nucleusContext == null)
466                                    throw new IllegalStateException("NucleusContext already garbage-collected!");
467    
468                            String macAlgorithmPropName = PROPERTY_MAC_ALGORITHM;
469                            String macAlgorithmPropValue = (String) nucleusContext.getPersistenceConfiguration().getProperty(macAlgorithmPropName);
470                            if (macAlgorithmPropValue == null || macAlgorithmPropValue.trim().isEmpty()) {
471                                    ma = MAC_ALGORITHM_NONE; // default value, if the property was not defined.
472    //                              ma = "HMAC-SHA1";
473                                    logger.info("getMACAlgorithm: Property '{}' is not set. Using default MAC algorithm '{}'.", macAlgorithmPropName, ma);
474                            }
475                            else {
476                                    ma = macAlgorithmPropValue.trim();
477                                    logger.info("getMACAlgorithm: Property '{}' is set to '{}'. Using this MAC algorithm.", macAlgorithmPropName, ma);
478                            }
479                            ma = ma.toUpperCase(Locale.ENGLISH);
480                            macAlgorithm = ma;
481                    }
482    
483                    return ma;
484            }
485            private String macAlgorithm = null;
486    }