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(); 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 }