001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.security; 029 030import org.opencms.configuration.CmsParameterConfiguration; 031import org.opencms.i18n.CmsEncoder; 032import org.opencms.i18n.CmsMessageContainer; 033import org.opencms.main.CmsLog; 034import org.opencms.util.CmsStringUtil; 035 036import java.io.UnsupportedEncodingException; 037import java.security.MessageDigest; 038import java.security.NoSuchAlgorithmException; 039import java.security.SecureRandom; 040import java.util.Locale; 041 042import org.apache.commons.codec.binary.Base64; 043import org.apache.commons.logging.Log; 044 045import com.lambdaworks.crypto.SCryptUtil; 046 047/** 048 * Default implementation for OpenCms password validation, 049 * just checks if a password is at last 4 characters long.<p> 050 * 051 * @since 6.0.0 052 */ 053public class CmsDefaultPasswordHandler 054implements I_CmsPasswordHandler, I_CmsPasswordSecurityEvaluator, I_CmsPasswordGenerator { 055 056 /** Parameter for SCrypt fall back. */ 057 public static String PARAM_SCRYPT_FALLBACK = "scrypt.fallback"; 058 059 /** Parameter for SCrypt settings. */ 060 public static String PARAM_SCRYPT_SETTINGS = "scrypt.settings"; 061 062 /** The minimum length of a password. */ 063 public static final int PASSWORD_MIN_LENGTH = 4; 064 065 /** The password length that is considered to be secure. */ 066 public static final int PASSWORD_SECURE_LENGTH = 8; 067 068 /** The log object for this class. */ 069 private static final Log LOG = CmsLog.getLog(CmsDefaultPasswordHandler.class); 070 071 /** The secure random number generator. */ 072 private static SecureRandom m_secureRandom; 073 074 /** The configuration of the password handler. */ 075 private CmsParameterConfiguration m_configuration; 076 077 /** The digest type used. */ 078 private String m_digestType = DIGEST_TYPE_SCRYPT; 079 080 /** The encoding the encoding used for translating the input string to bytes. */ 081 private String m_inputEncoding = CmsEncoder.ENCODING_UTF_8; 082 083 /** SCrypt fall back algorithm. */ 084 private String m_scryptFallback; 085 086 /** SCrypt parameter: CPU cost, must be a power of 2. */ 087 private int m_scryptN; 088 089 /** SCrypt parameter: Parallelization parameter. */ 090 private int m_scryptP; 091 092 /** SCrypt parameter: Memory cost. */ 093 private int m_scryptR; 094 095 /** 096 * The constructor does not perform any operation.<p> 097 */ 098 public CmsDefaultPasswordHandler() { 099 100 m_configuration = new CmsParameterConfiguration(); 101 } 102 103 /** 104 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 105 */ 106 public void addConfigurationParameter(String paramName, String paramValue) { 107 108 m_configuration.put(paramName, paramValue); 109 } 110 111 /** 112 * @see org.opencms.security.I_CmsPasswordHandler#checkPassword(String, String, boolean) 113 */ 114 public boolean checkPassword(String plainPassword, String digestedPassword, boolean useFallback) { 115 116 boolean success = false; 117 if (DIGEST_TYPE_PLAIN.equals(m_digestType)) { 118 119 success = plainPassword.equals(digestedPassword); 120 } else if (DIGEST_TYPE_SCRYPT.equals(m_digestType)) { 121 try { 122 success = SCryptUtil.check(plainPassword, digestedPassword); 123 } catch (IllegalArgumentException e) { 124 // hashed valued not right, check if we want to fall back to MD5 125 if (useFallback) { 126 try { 127 success = digestedPassword.equals(digest(plainPassword, m_scryptFallback, m_inputEncoding)); 128 } catch (CmsPasswordEncryptionException e1) { 129 // success will be false 130 } 131 } 132 } 133 } else { 134 // old default MD5 135 try { 136 success = digestedPassword.equals(digest(plainPassword)); 137 } catch (CmsPasswordEncryptionException e) { 138 // this indicates validation has failed 139 } 140 } 141 return success; 142 } 143 144 /** 145 * @see org.opencms.security.I_CmsPasswordHandler#digest(java.lang.String) 146 */ 147 public String digest(String password) throws CmsPasswordEncryptionException { 148 149 return digest(password, m_digestType, m_inputEncoding); 150 } 151 152 /** 153 * @see org.opencms.security.I_CmsPasswordHandler#digest(java.lang.String, java.lang.String, java.lang.String) 154 */ 155 public String digest(String password, String digestType, String inputEncoding) 156 throws CmsPasswordEncryptionException { 157 158 MessageDigest md; 159 String result; 160 161 try { 162 if (DIGEST_TYPE_PLAIN.equals(digestType.toLowerCase())) { 163 164 result = password; 165 166 } else if (DIGEST_TYPE_SCRYPT.equals(digestType.toLowerCase())) { 167 168 result = SCryptUtil.scrypt(password, m_scryptN, m_scryptR, m_scryptP); 169 } else if (DIGEST_TYPE_SSHA.equals(digestType.toLowerCase())) { 170 171 byte[] salt = new byte[4]; 172 byte[] digest; 173 byte[] total; 174 175 if (m_secureRandom == null) { 176 m_secureRandom = SecureRandom.getInstance("SHA1PRNG"); 177 } 178 m_secureRandom.nextBytes(salt); 179 180 md = MessageDigest.getInstance(DIGEST_TYPE_SHA); 181 md.reset(); 182 md.update(password.getBytes(inputEncoding)); 183 md.update(salt); 184 185 digest = md.digest(); 186 total = new byte[digest.length + salt.length]; 187 System.arraycopy(digest, 0, total, 0, digest.length); 188 System.arraycopy(salt, 0, total, digest.length, salt.length); 189 190 result = new String(Base64.encodeBase64(total)); 191 192 } else { 193 194 md = MessageDigest.getInstance(digestType); 195 md.reset(); 196 md.update(password.getBytes(inputEncoding)); 197 result = new String(Base64.encodeBase64(md.digest())); 198 199 } 200 } catch (NoSuchAlgorithmException e) { 201 CmsMessageContainer message = Messages.get().container(Messages.ERR_UNSUPPORTED_ALGORITHM_1, digestType); 202 if (LOG.isErrorEnabled()) { 203 LOG.error(message.key(), e); 204 } 205 throw new CmsPasswordEncryptionException(message, e); 206 } catch (UnsupportedEncodingException e) { 207 CmsMessageContainer message = Messages.get().container( 208 Messages.ERR_UNSUPPORTED_PASSWORD_ENCODING_1, 209 inputEncoding); 210 if (LOG.isErrorEnabled()) { 211 LOG.error(message.key(), e); 212 } 213 throw new CmsPasswordEncryptionException(message, e); 214 } 215 216 return result; 217 } 218 219 /** 220 * @see org.opencms.security.I_CmsPasswordSecurityEvaluator#evaluatePasswordSecurity(java.lang.String) 221 */ 222 public SecurityLevel evaluatePasswordSecurity(String password) { 223 224 SecurityLevel result; 225 if (password.length() < PASSWORD_MIN_LENGTH) { 226 result = SecurityLevel.invalid; 227 } else if (password.length() < PASSWORD_SECURE_LENGTH) { 228 result = SecurityLevel.weak; 229 } else { 230 result = SecurityLevel.strong; 231 } 232 return result; 233 } 234 235 /** 236 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 237 */ 238 public CmsParameterConfiguration getConfiguration() { 239 240 return m_configuration; 241 } 242 243 /** 244 * Returns the digestType.<p> 245 * 246 * @return the digestType 247 */ 248 public String getDigestType() { 249 250 return m_digestType; 251 } 252 253 /** 254 * Returns the input encoding.<p> 255 * 256 * @return the input encoding 257 */ 258 public String getInputEncoding() { 259 260 return m_inputEncoding; 261 } 262 263 /** 264 * @see org.opencms.security.I_CmsPasswordSecurityEvaluator#getPasswordSecurityHint(java.util.Locale) 265 */ 266 public String getPasswordSecurityHint(Locale locale) { 267 268 return Messages.get().getBundle(locale).key( 269 Messages.GUI_PASSWORD_SECURITY_HINT_1, 270 Integer.valueOf(PASSWORD_SECURE_LENGTH)); 271 } 272 273 /** 274 * @see org.opencms.security.I_CmsPasswordGenerator#getRandomPassword() 275 */ 276 public String getRandomPassword() { 277 278 return CmsDefaultPasswordGenerator.getRandomPWD(); 279 } 280 281 /** 282 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 283 */ 284 public void initConfiguration() { 285 286 // simple default configuration does not need to be initialized 287 if (LOG.isDebugEnabled()) { 288 CmsMessageContainer message = Messages.get().container(Messages.LOG_INIT_CONFIG_CALLED_1, this); 289 LOG.debug(message.key()); 290 LOG.debug(Messages.get().getBundle().key(Messages.LOG_INIT_CONFIG_CALLED_1, this)); 291 } 292 m_configuration = CmsParameterConfiguration.unmodifiableVersion(m_configuration); 293 294 // Set default SCrypt parameter values 295 m_scryptN = 16384; // CPU cost, must be a power of 2 296 m_scryptR = 8; // Memory cost 297 m_scryptP = 1; // Parallelization parameter 298 299 String scryptSettings = m_configuration.get(PARAM_SCRYPT_SETTINGS); 300 if (scryptSettings != null) { 301 String[] settings = CmsStringUtil.splitAsArray(scryptSettings, ','); 302 if (settings.length == 3) { 303 // we just require 3 correct parameters 304 m_scryptN = CmsStringUtil.getIntValue(settings[0], m_scryptN, "scryptN using " + m_scryptN); 305 m_scryptR = CmsStringUtil.getIntValue(settings[1], m_scryptR, "scryptR using " + m_scryptR); 306 m_scryptP = CmsStringUtil.getIntValue(settings[2], m_scryptP, "scryptP using " + m_scryptP); 307 } else { 308 if (LOG.isDebugEnabled()) { 309 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SCRYPT_PARAMETERS_1, scryptSettings)); 310 } 311 } 312 } 313 314 // Initialize the SCrypt fall back 315 m_scryptFallback = DIGEST_TYPE_MD5; 316 String scryptFallback = m_configuration.get(PARAM_SCRYPT_FALLBACK); 317 if (scryptFallback != null) { 318 319 try { 320 MessageDigest.getInstance(scryptFallback); 321 // Configured fall back algorithm available 322 m_scryptFallback = scryptFallback; 323 } catch (NoSuchAlgorithmException e) { 324 // Configured fall back algorithm not available, use default MD5 325 if (LOG.isDebugEnabled()) { 326 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SCRYPT_PARAMETERS_1, scryptFallback)); 327 } 328 } 329 } 330 } 331 332 /** 333 * Sets the digestType.<p> 334 * 335 * @param digestType the digestType to set 336 */ 337 public void setDigestType(String digestType) { 338 339 m_digestType = digestType.toLowerCase(); 340 } 341 342 /** 343 * Sets the input encoding.<p> 344 * 345 * @param inputEncoding the input encoding to set 346 */ 347 public void setInputEncoding(String inputEncoding) { 348 349 m_inputEncoding = inputEncoding; 350 } 351 352 /** 353 * @see org.opencms.security.I_CmsPasswordHandler#validatePassword(java.lang.String) 354 */ 355 public void validatePassword(String password) throws CmsSecurityException { 356 357 if ((password == null) || (password.length() < PASSWORD_MIN_LENGTH)) { 358 throw new CmsSecurityException( 359 Messages.get().container(Messages.ERR_PASSWORD_TOO_SHORT_1, Integer.valueOf(PASSWORD_MIN_LENGTH))); 360 } 361 } 362}