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