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