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