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