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 }