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 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.SaltedSHA384PasswordStorageSchemeCfg; 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 * 384-bit SHA-2 algorithm defined in FIPS 180-2. This is a one-way digest 046 * algorithm so there is no way to retrieve the original clear-text version of 047 * the password from the hashed value (although this means that it is not 048 * suitable for things that need the clear-text password like DIGEST-MD5). The 049 * values that it generates are also salted, which protects against dictionary 050 * attacks. It does this by generating a 64-bit random salt which is appended to 051 * the clear-text value. A SHA-2 hash is then generated based on this, the salt 052 * is appended to the hash, and then the entire value is base64-encoded. 053 */ 054public class SaltedSHA384PasswordStorageScheme 055 extends PasswordStorageScheme<SaltedSHA384PasswordStorageSchemeCfg> 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.SaltedSHA384PasswordStorageScheme"; 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 074 /** The size of the digest in bytes. */ 075 private static final int SHA384_LENGTH = 384 / 8; 076 077 /** 078 * The message digest that will actually be used to generate the 384-bit SHA-2 079 * hashes. 080 */ 081 private MessageDigest messageDigest; 082 083 /** The lock used to provide threadsafe access to the message digest. */ 084 private Object digestLock; 085 086 /** The secure random number generator to use to generate the salt values. */ 087 private Random random; 088 089 090 091 /** 092 * Creates a new instance of this password storage scheme. Note that no 093 * initialization should be performed here, as all initialization should be 094 * done in the <CODE>initializePasswordStorageScheme</CODE> method. 095 */ 096 public SaltedSHA384PasswordStorageScheme() 097 { 098 super(); 099 } 100 101 102 103 /** {@inheritDoc} */ 104 @Override 105 public void initializePasswordStorageScheme( 106 SaltedSHA384PasswordStorageSchemeCfg configuration) 107 throws ConfigException, InitializationException 108 { 109 try 110 { 111 messageDigest = 112 MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384); 113 } 114 catch (Exception e) 115 { 116 logger.traceException(e); 117 118 LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get( 119 MESSAGE_DIGEST_ALGORITHM_SHA_384, e); 120 throw new InitializationException(message, e); 121 } 122 123 124 digestLock = new Object(); 125 random = new Random(); 126 } 127 128 129 130 /** {@inheritDoc} */ 131 @Override 132 public String getStorageSchemeName() 133 { 134 return STORAGE_SCHEME_NAME_SALTED_SHA_384; 135 } 136 137 138 139 /** {@inheritDoc} */ 140 @Override 141 public ByteString encodePassword(ByteSequence plaintext) 142 throws DirectoryException 143 { 144 int plainBytesLength = plaintext.length(); 145 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 146 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 147 148 plaintext.copyTo(plainPlusSalt); 149 150 byte[] digestBytes; 151 152 synchronized (digestLock) 153 { 154 try 155 { 156 // Generate the salt and put in the plain+salt array. 157 random.nextBytes(saltBytes); 158 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 159 NUM_SALT_BYTES); 160 161 // Create the hash from the concatenated value. 162 digestBytes = messageDigest.digest(plainPlusSalt); 163 } 164 catch (Exception e) 165 { 166 logger.traceException(e); 167 168 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 169 CLASS_NAME, getExceptionMessage(e)); 170 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 171 message, e); 172 } 173 finally 174 { 175 Arrays.fill(plainPlusSalt, (byte) 0); 176 } 177 } 178 179 // Append the salt to the hashed value and base64-the whole thing. 180 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 181 182 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 183 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 184 NUM_SALT_BYTES); 185 186 return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt)); 187 } 188 189 190 191 /** {@inheritDoc} */ 192 @Override 193 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 194 throws DirectoryException 195 { 196 StringBuilder buffer = new StringBuilder(); 197 buffer.append('{'); 198 buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_384); 199 buffer.append('}'); 200 201 int plainBytesLength = plaintext.length(); 202 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 203 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 204 205 plaintext.copyTo(plainPlusSalt); 206 207 byte[] digestBytes; 208 209 synchronized (digestLock) 210 { 211 try 212 { 213 // Generate the salt and put in the plain+salt array. 214 random.nextBytes(saltBytes); 215 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 216 NUM_SALT_BYTES); 217 218 // Create the hash from the concatenated value. 219 digestBytes = messageDigest.digest(plainPlusSalt); 220 } 221 catch (Exception e) 222 { 223 logger.traceException(e); 224 225 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 226 CLASS_NAME, getExceptionMessage(e)); 227 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 228 message, e); 229 } 230 finally 231 { 232 Arrays.fill(plainPlusSalt, (byte) 0); 233 } 234 } 235 236 // Append the salt to the hashed value and base64-the whole thing. 237 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 238 239 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 240 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 241 NUM_SALT_BYTES); 242 buffer.append(Base64.encode(hashPlusSalt)); 243 244 return ByteString.valueOfUtf8(buffer); 245 } 246 247 248 249 /** {@inheritDoc} */ 250 @Override 251 public boolean passwordMatches(ByteSequence plaintextPassword, 252 ByteSequence storedPassword) 253 { 254 // Base64-decode the stored value and take the first 384 bits 255 // (SHA384_LENGTH) as the digest. 256 byte[] saltBytes; 257 byte[] digestBytes = new byte[SHA384_LENGTH]; 258 int saltLength = 0; 259 260 try 261 { 262 byte[] decodedBytes = Base64.decode(storedPassword.toString()); 263 264 saltLength = decodedBytes.length - SHA384_LENGTH; 265 if (saltLength <= 0) 266 { 267 logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); 268 return false; 269 } 270 saltBytes = new byte[saltLength]; 271 System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA384_LENGTH); 272 System.arraycopy(decodedBytes, SHA384_LENGTH, saltBytes, 0, 273 saltLength); 274 } 275 catch (Exception e) 276 { 277 logger.traceException(e); 278 logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); 279 return false; 280 } 281 282 283 // Use the salt to generate a digest based on the provided plain-text value. 284 int plainBytesLength = plaintextPassword.length(); 285 byte[] plainPlusSalt = new byte[plainBytesLength + saltLength]; 286 plaintextPassword.copyTo(plainPlusSalt); 287 System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytesLength, 288 saltLength); 289 290 byte[] userDigestBytes; 291 292 synchronized (digestLock) 293 { 294 try 295 { 296 userDigestBytes = messageDigest.digest(plainPlusSalt); 297 } 298 catch (Exception e) 299 { 300 logger.traceException(e); 301 302 return false; 303 } 304 finally 305 { 306 Arrays.fill(plainPlusSalt, (byte) 0); 307 } 308 } 309 310 return Arrays.equals(digestBytes, userDigestBytes); 311 } 312 313 314 315 /** {@inheritDoc} */ 316 @Override 317 public boolean supportsAuthPasswordSyntax() 318 { 319 // This storage scheme does support the authentication password syntax. 320 return true; 321 } 322 323 324 325 /** {@inheritDoc} */ 326 @Override 327 public String getAuthPasswordSchemeName() 328 { 329 return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384; 330 } 331 332 333 334 /** {@inheritDoc} */ 335 @Override 336 public ByteString encodeAuthPassword(ByteSequence plaintext) 337 throws DirectoryException 338 { 339 int plaintextLength = plaintext.length(); 340 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 341 byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES]; 342 343 plaintext.copyTo(plainPlusSalt); 344 345 byte[] digestBytes; 346 347 synchronized (digestLock) 348 { 349 try 350 { 351 // Generate the salt and put in the plain+salt array. 352 random.nextBytes(saltBytes); 353 System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, 354 NUM_SALT_BYTES); 355 356 // Create the hash from the concatenated value. 357 digestBytes = messageDigest.digest(plainPlusSalt); 358 } 359 catch (Exception e) 360 { 361 logger.traceException(e); 362 363 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 364 CLASS_NAME, getExceptionMessage(e)); 365 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 366 message, e); 367 } 368 finally 369 { 370 Arrays.fill(plainPlusSalt, (byte) 0); 371 } 372 } 373 374 375 // Encode and return the value. 376 StringBuilder authPWValue = new StringBuilder(); 377 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384); 378 authPWValue.append('$'); 379 authPWValue.append(Base64.encode(saltBytes)); 380 authPWValue.append('$'); 381 authPWValue.append(Base64.encode(digestBytes)); 382 383 return ByteString.valueOfUtf8(authPWValue); 384 } 385 386 387 388 /** {@inheritDoc} */ 389 @Override 390 public boolean authPasswordMatches(ByteSequence plaintextPassword, 391 String authInfo, String authValue) 392 { 393 byte[] saltBytes; 394 byte[] digestBytes; 395 try 396 { 397 saltBytes = Base64.decode(authInfo); 398 digestBytes = Base64.decode(authValue); 399 } 400 catch (Exception e) 401 { 402 logger.traceException(e); 403 404 return false; 405 } 406 407 408 int plainBytesLength = plaintextPassword.length(); 409 byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length]; 410 plaintextPassword.copyTo(plainPlusSaltBytes); 411 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, 412 saltBytes.length); 413 414 synchronized (digestLock) 415 { 416 try 417 { 418 return Arrays.equals(digestBytes, 419 messageDigest.digest(plainPlusSaltBytes)); 420 } 421 finally 422 { 423 Arrays.fill(plainPlusSaltBytes, (byte) 0); 424 } 425 } 426 } 427 428 429 430 /** {@inheritDoc} */ 431 @Override 432 public boolean isReversible() 433 { 434 return false; 435 } 436 437 438 439 /** {@inheritDoc} */ 440 @Override 441 public ByteString getPlaintextValue(ByteSequence storedPassword) 442 throws DirectoryException 443 { 444 LocalizableMessage message = 445 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_384); 446 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 447 } 448 449 450 451 /** {@inheritDoc} */ 452 @Override 453 public ByteString getAuthPasswordPlaintextValue(String authInfo, 454 String authValue) 455 throws DirectoryException 456 { 457 LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get( 458 AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384); 459 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 460 } 461 462 463 464 /** {@inheritDoc} */ 465 @Override 466 public boolean isStorageSchemeSecure() 467 { 468 // SHA-2 should be considered secure. 469 return true; 470 } 471} 472