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