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