001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2008 Sun Microsystems, Inc. 025 * Portions Copyright 2013-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029 030 031import java.security.MessageDigest; 032import java.util.Arrays; 033import java.util.Random; 034 035import org.forgerock.i18n.LocalizableMessage; 036import org.opends.server.admin.std.server.SaltedMD5PasswordStorageSchemeCfg; 037import org.opends.server.api.PasswordStorageScheme; 038import org.forgerock.opendj.config.server.ConfigException; 039import org.opends.server.core.DirectoryServer; 040import org.forgerock.i18n.slf4j.LocalizedLogger; 041import org.opends.server.types.*; 042import org.forgerock.opendj.ldap.ResultCode; 043import org.forgerock.opendj.ldap.ByteString; 044import org.forgerock.opendj.ldap.ByteSequence; 045import org.opends.server.util.Base64; 046 047import static org.opends.messages.ExtensionMessages.*; 048import static org.opends.server.extensions.ExtensionsConstants.*; 049import static org.opends.server.util.StaticUtils.*; 050 051 052 053/** 054 * This class defines a Directory Server password storage scheme based on the 055 * MD5 algorithm defined in RFC 1321. This is a one-way digest algorithm so 056 * there is no way to retrieve the original clear-text version of the 057 * password from the hashed value (although this means that it is not suitable 058 * for things that need the clear-text password like DIGEST-MD5). The values 059 * that it generates are also salted, which protects against dictionary attacks. 060 * It does this by generating a 64-bit random salt which is appended to the 061 * clear-text value. A MD5 hash is then generated based on this, the salt is 062 * appended to the hash, and then the entire value is base64-encoded. 063 */ 064public class SaltedMD5PasswordStorageScheme 065 extends PasswordStorageScheme<SaltedMD5PasswordStorageSchemeCfg> 066{ 067 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 068 069 /** 070 * The fully-qualified name of this class. 071 */ 072 private static final String CLASS_NAME = 073 "org.opends.server.extensions.SaltedMD5PasswordStorageScheme"; 074 075 076 077 /** 078 * The number of bytes of random data to use as the salt when generating the 079 * hashes. 080 */ 081 private static final int NUM_SALT_BYTES = 8; 082 083 /** The number of bytes MD5 algorithm produces. */ 084 private static final int MD5_LENGTH = 16; 085 086 087 /** The message digest that will actually be used to generate the MD5 hashes. */ 088 private MessageDigest messageDigest; 089 090 /** The lock used to provide threadsafe access to the message digest. */ 091 private Object digestLock; 092 093 /** The secure random number generator to use to generate the salt values. */ 094 private Random random; 095 096 097 098 /** 099 * Creates a new instance of this password storage scheme. Note that no 100 * initialization should be performed here, as all initialization should be 101 * done in the <CODE>initializePasswordStorageScheme</CODE> method. 102 */ 103 public SaltedMD5PasswordStorageScheme() 104 { 105 super(); 106 } 107 108 109 110 /** {@inheritDoc} */ 111 @Override 112 public void initializePasswordStorageScheme( 113 SaltedMD5PasswordStorageSchemeCfg configuration) 114 throws ConfigException, InitializationException 115 { 116 try 117 { 118 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); 119 } 120 catch (Exception e) 121 { 122 logger.traceException(e); 123 124 LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(MESSAGE_DIGEST_ALGORITHM_MD5, e); 125 throw new InitializationException(message, e); 126 } 127 128 129 digestLock = new Object(); 130 random = new Random(); 131 } 132 133 134 135 /** {@inheritDoc} */ 136 @Override 137 public String getStorageSchemeName() 138 { 139 return STORAGE_SCHEME_NAME_SALTED_MD5; 140 } 141 142 143 144 /** {@inheritDoc} */ 145 @Override 146 public ByteString encodePassword(ByteSequence plaintext) 147 throws DirectoryException 148 { 149 int plainBytesLength = plaintext.length(); 150 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 151 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 152 153 plaintext.copyTo(plainPlusSalt); 154 155 byte[] digestBytes; 156 157 synchronized (digestLock) 158 { 159 try 160 { 161 // Generate the salt and put in the plain+salt array. 162 random.nextBytes(saltBytes); 163 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 164 NUM_SALT_BYTES); 165 166 // Create the hash from the concatenated value. 167 digestBytes = messageDigest.digest(plainPlusSalt); 168 } 169 catch (Exception e) 170 { 171 logger.traceException(e); 172 173 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 174 CLASS_NAME, getExceptionMessage(e)); 175 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 176 message, e); 177 } 178 finally 179 { 180 Arrays.fill(plainPlusSalt, (byte) 0); 181 } 182 } 183 184 // Append the salt to the hashed value and base64-the whole thing. 185 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 186 187 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 188 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 189 NUM_SALT_BYTES); 190 191 return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt)); 192 } 193 194 195 196 /** {@inheritDoc} */ 197 @Override 198 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 199 throws DirectoryException 200 { 201 StringBuilder buffer = new StringBuilder(); 202 buffer.append('{'); 203 buffer.append(STORAGE_SCHEME_NAME_SALTED_MD5); 204 buffer.append('}'); 205 206 int plainBytesLength = plaintext.length(); 207 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 208 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 209 210 plaintext.copyTo(plainPlusSalt); 211 212 byte[] digestBytes; 213 214 synchronized (digestLock) 215 { 216 try 217 { 218 // Generate the salt and put in the plain+salt array. 219 random.nextBytes(saltBytes); 220 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 221 NUM_SALT_BYTES); 222 223 // Create the hash from the concatenated value. 224 digestBytes = messageDigest.digest(plainPlusSalt); 225 } 226 catch (Exception e) 227 { 228 logger.traceException(e); 229 230 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 231 CLASS_NAME, getExceptionMessage(e)); 232 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 233 message, e); 234 } 235 finally 236 { 237 Arrays.fill(plainPlusSalt, (byte) 0); 238 } 239 } 240 241 // Append the salt to the hashed value and base64-the whole thing. 242 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 243 244 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 245 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 246 NUM_SALT_BYTES); 247 buffer.append(Base64.encode(hashPlusSalt)); 248 249 return ByteString.valueOfUtf8(buffer); 250 } 251 252 253 254 /** {@inheritDoc} */ 255 @Override 256 public boolean passwordMatches(ByteSequence plaintextPassword, 257 ByteSequence storedPassword) 258 { 259 // Base64-decode the stored value and take the last 8 bytes as the salt. 260 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 261 byte[] digestBytes = new byte[MD5_LENGTH]; 262 int saltLength = 0; 263 try 264 { 265 byte[] decodedBytes = Base64.decode(storedPassword.toString()); 266 267 saltLength = decodedBytes.length - MD5_LENGTH; 268 if (saltLength <= 0) 269 { 270 logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); 271 return false; 272 } 273 saltBytes = new byte[saltLength]; 274 System.arraycopy(decodedBytes, 0, digestBytes, 0, MD5_LENGTH); 275 System.arraycopy(decodedBytes, MD5_LENGTH, saltBytes, 0, 276 saltLength); 277 } 278 catch (Exception e) 279 { 280 logger.traceException(e); 281 logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); 282 return false; 283 } 284 285 286 // Use the salt to generate a digest based on the provided plain-text value. 287 int plainBytesLength = plaintextPassword.length(); 288 byte[] plainPlusSalt = new byte[plainBytesLength + saltLength]; 289 plaintextPassword.copyTo(plainPlusSalt); 290 System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength, 291 saltLength); 292 293 byte[] userDigestBytes; 294 295 synchronized (digestLock) 296 { 297 try 298 { 299 userDigestBytes = messageDigest.digest(plainPlusSalt); 300 } 301 catch (Exception e) 302 { 303 logger.traceException(e); 304 305 return false; 306 } 307 finally 308 { 309 Arrays.fill(plainPlusSalt, (byte) 0); 310 } 311 } 312 313 return Arrays.equals(digestBytes, userDigestBytes); 314 } 315 316 317 318 /** {@inheritDoc} */ 319 @Override 320 public boolean supportsAuthPasswordSyntax() 321 { 322 // This storage scheme does support the authentication password syntax. 323 return true; 324 } 325 326 327 328 /** {@inheritDoc} */ 329 @Override 330 public String getAuthPasswordSchemeName() 331 { 332 return AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5; 333 } 334 335 336 337 /** {@inheritDoc} */ 338 @Override 339 public ByteString encodeAuthPassword(ByteSequence plaintext) 340 throws DirectoryException 341 { 342 int plaintextLength = plaintext.length(); 343 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 344 byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES]; 345 346 plaintext.copyTo(plainPlusSalt); 347 348 byte[] digestBytes; 349 350 synchronized (digestLock) 351 { 352 try 353 { 354 // Generate the salt and put in the plain+salt array. 355 random.nextBytes(saltBytes); 356 System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, 357 NUM_SALT_BYTES); 358 359 // Create the hash from the concatenated value. 360 digestBytes = messageDigest.digest(plainPlusSalt); 361 } 362 catch (Exception e) 363 { 364 logger.traceException(e); 365 366 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 367 CLASS_NAME, getExceptionMessage(e)); 368 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 369 message, e); 370 } 371 finally 372 { 373 Arrays.fill(plainPlusSalt, (byte) 0); 374 } 375 } 376 377 378 // Encode and return the value. 379 StringBuilder authPWValue = new StringBuilder(); 380 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5); 381 authPWValue.append('$'); 382 authPWValue.append(Base64.encode(saltBytes)); 383 authPWValue.append('$'); 384 authPWValue.append(Base64.encode(digestBytes)); 385 386 return ByteString.valueOfUtf8(authPWValue); 387 } 388 389 390 391 /** {@inheritDoc} */ 392 @Override 393 public boolean authPasswordMatches(ByteSequence plaintextPassword, 394 String authInfo, String authValue) 395 { 396 byte[] saltBytes; 397 byte[] digestBytes; 398 try 399 { 400 saltBytes = Base64.decode(authInfo); 401 digestBytes = Base64.decode(authValue); 402 } 403 catch (Exception e) 404 { 405 logger.traceException(e); 406 407 return false; 408 } 409 410 411 int plainBytesLength = plaintextPassword.length(); 412 byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length]; 413 plaintextPassword.copyTo(plainPlusSaltBytes); 414 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, 415 saltBytes.length); 416 417 synchronized (digestLock) 418 { 419 try 420 { 421 return Arrays.equals(digestBytes, 422 messageDigest.digest(plainPlusSaltBytes)); 423 } 424 finally 425 { 426 Arrays.fill(plainPlusSaltBytes, (byte) 0); 427 } 428 } 429 } 430 431 432 433 /** {@inheritDoc} */ 434 @Override 435 public boolean isReversible() 436 { 437 return false; 438 } 439 440 441 442 /** {@inheritDoc} */ 443 @Override 444 public ByteString getPlaintextValue(ByteSequence storedPassword) 445 throws DirectoryException 446 { 447 LocalizableMessage message = 448 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_MD5); 449 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 450 } 451 452 453 454 /** {@inheritDoc} */ 455 @Override 456 public ByteString getAuthPasswordPlaintextValue(String authInfo, 457 String authValue) 458 throws DirectoryException 459 { 460 LocalizableMessage message = 461 ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5); 462 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 463 } 464 465 466 467 /** {@inheritDoc} */ 468 @Override 469 public boolean isStorageSchemeSecure() 470 { 471 // MD5 may be considered reasonably secure for this purpose. 472 return true; 473 } 474} 475