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