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.SaltedSHA1PasswordStorageSchemeCfg; 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 * SHA-1 algorithm defined in FIPS 180-1. This is a one-way digest algorithm 060 * so 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 SHA-1 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 SaltedSHA1PasswordStorageScheme 069 extends PasswordStorageScheme<SaltedSHA1PasswordStorageSchemeCfg> 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.SaltedSHA1PasswordStorageScheme"; 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 SHA-1 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 SaltedSHA1PasswordStorageScheme() 109 { 110 super(); 111 } 112 113 114 115 /** 116 * {@inheritDoc} 117 */ 118 @Override() 119 public void initializePasswordStorageScheme( 120 SaltedSHA1PasswordStorageSchemeCfg configuration) 121 throws ConfigException, InitializationException 122 { 123 try 124 { 125 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); 126 } 127 catch (Exception e) 128 { 129 if (debugEnabled()) 130 { 131 TRACER.debugCaught(DebugLogLevel.ERROR, e); 132 } 133 134 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get( 135 MESSAGE_DIGEST_ALGORITHM_SHA_1, String.valueOf(e)); 136 throw new InitializationException(message, e); 137 } 138 139 digestLock = new Object(); 140 random = new Random(); 141 } 142 143 144 145 /** 146 * {@inheritDoc} 147 */ 148 @Override() 149 public String getStorageSchemeName() 150 { 151 return STORAGE_SCHEME_NAME_SALTED_SHA_1; 152 } 153 154 155 156 /** 157 * {@inheritDoc} 158 */ 159 @Override() 160 public ByteString encodePassword(ByteString plaintext) 161 throws DirectoryException 162 { 163 byte[] plainBytes = plaintext.value(); 164 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 165 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES]; 166 167 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length); 168 169 byte[] digestBytes; 170 171 synchronized (digestLock) 172 { 173 try 174 { 175 // Generate the salt and put in the plain+salt array. 176 random.nextBytes(saltBytes); 177 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length, 178 NUM_SALT_BYTES); 179 180 // Create the hash from the concatenated value. 181 digestBytes = messageDigest.digest(plainPlusSalt); 182 } 183 catch (Exception e) 184 { 185 if (debugEnabled()) 186 { 187 TRACER.debugCaught(DebugLogLevel.ERROR, e); 188 } 189 190 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 191 CLASS_NAME, getExceptionMessage(e)); 192 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 193 message, e); 194 } 195 } 196 197 // Append the salt to the hashed value and base64-the whole thing. 198 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 199 200 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 201 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 202 NUM_SALT_BYTES); 203 204 return ByteStringFactory.create(Base64.encode(hashPlusSalt)); 205 } 206 207 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override() 213 public ByteString encodePasswordWithScheme(ByteString plaintext) 214 throws DirectoryException 215 { 216 StringBuilder buffer = new StringBuilder(); 217 buffer.append('{'); 218 buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_1); 219 buffer.append('}'); 220 221 byte[] plainBytes = plaintext.value(); 222 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 223 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES]; 224 225 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length); 226 227 byte[] digestBytes; 228 229 synchronized (digestLock) 230 { 231 try 232 { 233 // Generate the salt and put in the plain+salt array. 234 random.nextBytes(saltBytes); 235 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length, 236 NUM_SALT_BYTES); 237 238 // Create the hash from the concatenated value. 239 digestBytes = messageDigest.digest(plainPlusSalt); 240 } 241 catch (Exception e) 242 { 243 if (debugEnabled()) 244 { 245 TRACER.debugCaught(DebugLogLevel.ERROR, e); 246 } 247 248 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 249 CLASS_NAME, getExceptionMessage(e)); 250 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 251 message, e); 252 } 253 } 254 255 // Append the salt to the hashed value and base64-the whole thing. 256 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 257 258 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 259 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 260 NUM_SALT_BYTES); 261 buffer.append(Base64.encode(hashPlusSalt)); 262 263 return ByteStringFactory.create(buffer.toString()); 264 } 265 266 267 268 /** 269 * {@inheritDoc} 270 */ 271 @Override() 272 public boolean passwordMatches(ByteString plaintextPassword, 273 ByteString storedPassword) 274 { 275 // Base64-decode the stored value and take the last 8 bytes as the salt. 276 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 277 byte[] digestBytes; 278 try 279 { 280 byte[] decodedBytes = Base64.decode(storedPassword.stringValue()); 281 282 int digestLength = decodedBytes.length - NUM_SALT_BYTES; 283 digestBytes = new byte[digestLength]; 284 System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength); 285 System.arraycopy(decodedBytes, digestLength, saltBytes, 0, 286 NUM_SALT_BYTES); 287 } 288 catch (Exception e) 289 { 290 if (debugEnabled()) 291 { 292 TRACER.debugCaught(DebugLogLevel.ERROR, e); 293 } 294 295 Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get( 296 storedPassword.stringValue(), String.valueOf(e)); 297 ErrorLogger.logError(message); 298 return false; 299 } 300 301 302 // Use the salt to generate a digest based on the provided plain-text value. 303 byte[] plainBytes = plaintextPassword.value(); 304 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES]; 305 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length); 306 System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytes.length, 307 NUM_SALT_BYTES); 308 309 byte[] userDigestBytes; 310 311 synchronized (digestLock) 312 { 313 try 314 { 315 userDigestBytes = messageDigest.digest(plainPlusSalt); 316 } 317 catch (Exception e) 318 { 319 if (debugEnabled()) 320 { 321 TRACER.debugCaught(DebugLogLevel.ERROR, e); 322 } 323 324 return false; 325 } 326 } 327 328 return Arrays.equals(digestBytes, userDigestBytes); 329 } 330 331 332 333 /** 334 * {@inheritDoc} 335 */ 336 @Override() 337 public boolean supportsAuthPasswordSyntax() 338 { 339 // This storage scheme does support the authentication password syntax. 340 return true; 341 } 342 343 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override() 349 public String getAuthPasswordSchemeName() 350 { 351 return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1; 352 } 353 354 355 356 /** 357 * {@inheritDoc} 358 */ 359 @Override() 360 public ByteString encodeAuthPassword(ByteString plaintext) 361 throws DirectoryException 362 { 363 byte[] plainBytes = plaintext.value(); 364 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 365 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES]; 366 367 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length); 368 369 byte[] digestBytes; 370 371 synchronized (digestLock) 372 { 373 try 374 { 375 // Generate the salt and put in the plain+salt array. 376 random.nextBytes(saltBytes); 377 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length, 378 NUM_SALT_BYTES); 379 380 // Create the hash from the concatenated value. 381 digestBytes = messageDigest.digest(plainPlusSalt); 382 } 383 catch (Exception e) 384 { 385 if (debugEnabled()) 386 { 387 TRACER.debugCaught(DebugLogLevel.ERROR, e); 388 } 389 390 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 391 CLASS_NAME, getExceptionMessage(e)); 392 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 393 message, e); 394 } 395 } 396 397 398 // Encode and return the value. 399 StringBuilder authPWValue = new StringBuilder(); 400 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1); 401 authPWValue.append('$'); 402 authPWValue.append(Base64.encode(saltBytes)); 403 authPWValue.append('$'); 404 authPWValue.append(Base64.encode(digestBytes)); 405 406 return ByteStringFactory.create(authPWValue.toString()); 407 } 408 409 410 411 /** 412 * {@inheritDoc} 413 */ 414 @Override() 415 public boolean authPasswordMatches(ByteString plaintextPassword, 416 String authInfo, String authValue) 417 { 418 byte[] saltBytes; 419 byte[] digestBytes; 420 try 421 { 422 saltBytes = Base64.decode(authInfo); 423 digestBytes = Base64.decode(authValue); 424 } 425 catch (Exception e) 426 { 427 if (debugEnabled()) 428 { 429 TRACER.debugCaught(DebugLogLevel.ERROR, e); 430 } 431 432 return false; 433 } 434 435 436 byte[] plainBytes = plaintextPassword.value(); 437 byte[] plainPlusSaltBytes = new byte[plainBytes.length + saltBytes.length]; 438 System.arraycopy(plainBytes, 0, plainPlusSaltBytes, 0, plainBytes.length); 439 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytes.length, 440 saltBytes.length); 441 442 synchronized (digestLock) 443 { 444 return Arrays.equals(digestBytes, 445 messageDigest.digest(plainPlusSaltBytes)); 446 } 447 } 448 449 450 451 /** 452 * {@inheritDoc} 453 */ 454 @Override() 455 public boolean isReversible() 456 { 457 return false; 458 } 459 460 461 462 /** 463 * {@inheritDoc} 464 */ 465 @Override() 466 public ByteString getPlaintextValue(ByteString storedPassword) 467 throws DirectoryException 468 { 469 Message message = 470 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_1); 471 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 472 } 473 474 475 476 /** 477 * {@inheritDoc} 478 */ 479 @Override() 480 public ByteString getAuthPasswordPlaintextValue(String authInfo, 481 String authValue) 482 throws DirectoryException 483 { 484 Message message = 485 ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1); 486 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 487 } 488 489 490 491 /** 492 * {@inheritDoc} 493 */ 494 @Override() 495 public boolean isStorageSchemeSecure() 496 { 497 // SHA-1 should be considered secure. 498 return true; 499 } 500 501 502 503 /** 504 * Generates an encoded password string from the given clear-text password. 505 * This method is primarily intended for use when it is necessary to generate 506 * a password with the server offline (e.g., when setting the initial root 507 * user password). 508 * 509 * @param passwordBytes The bytes that make up the clear-text password. 510 * 511 * @return The encoded password string, including the scheme name in curly 512 * braces. 513 * 514 * @throws DirectoryException If a problem occurs during processing. 515 */ 516 public static String encodeOffline(byte[] passwordBytes) 517 throws DirectoryException 518 { 519 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 520 new Random().nextBytes(saltBytes); 521 522 byte[] passwordPlusSalt = new byte[passwordBytes.length + NUM_SALT_BYTES]; 523 System.arraycopy(passwordBytes, 0, passwordPlusSalt, 0, 524 passwordBytes.length); 525 System.arraycopy(saltBytes, 0, passwordPlusSalt, passwordBytes.length, 526 NUM_SALT_BYTES); 527 528 MessageDigest messageDigest; 529 try 530 { 531 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); 532 } 533 catch (Exception e) 534 { 535 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get( 536 MESSAGE_DIGEST_ALGORITHM_SHA_1, String.valueOf(e)); 537 throw new DirectoryException(ResultCode.OTHER, message, e); 538 } 539 540 541 byte[] digestBytes = messageDigest.digest(passwordPlusSalt); 542 byte[] digestPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 543 System.arraycopy(digestBytes, 0, digestPlusSalt, 0, digestBytes.length); 544 System.arraycopy(saltBytes, 0, digestPlusSalt, digestBytes.length, 545 NUM_SALT_BYTES); 546 547 return "{" + STORAGE_SCHEME_NAME_SALTED_SHA_1 + "}" + 548 Base64.encode(digestPlusSalt); 549 } 550 } 551