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