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 2013-2015 ForgeRock AS. 015 */ 016package org.opends.server.extensions; 017 018import java.security.NoSuchAlgorithmException; 019import java.security.SecureRandom; 020import java.security.spec.KeySpec; 021import java.util.Arrays; 022import java.util.List; 023 024import javax.crypto.SecretKeyFactory; 025import javax.crypto.spec.PBEKeySpec; 026 027import org.forgerock.i18n.LocalizableMessage; 028import org.forgerock.i18n.slf4j.LocalizedLogger; 029import org.forgerock.opendj.config.server.ConfigException; 030import org.forgerock.opendj.ldap.ByteSequence; 031import org.forgerock.opendj.ldap.ByteString; 032import org.forgerock.opendj.ldap.ResultCode; 033import org.opends.server.admin.server.ConfigurationChangeListener; 034import org.opends.server.admin.std.server.PBKDF2PasswordStorageSchemeCfg; 035import org.opends.server.api.PasswordStorageScheme; 036import org.opends.server.core.DirectoryServer; 037import org.forgerock.opendj.config.server.ConfigChangeResult; 038import org.opends.server.types.DirectoryException; 039import org.opends.server.types.InitializationException; 040import org.opends.server.util.Base64; 041 042import static org.opends.messages.ExtensionMessages.*; 043import static org.opends.server.extensions.ExtensionsConstants.*; 044import static org.opends.server.util.StaticUtils.*; 045 046/** 047 * This class defines a Directory Server password storage scheme based on the 048 * PBKDF2 algorithm defined in RFC 2898. This is a one-way digest algorithm 049 * so there is no way to retrieve the original clear-text version of the 050 * password from the hashed value (although this means that it is not suitable 051 * for things that need the clear-text password like DIGEST-MD5). This 052 * implementation uses a configurable number of iterations. 053 */ 054public class PBKDF2PasswordStorageScheme 055 extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg> 056 implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg> 057{ 058 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 059 060 /** The fully-qualified name of this class. */ 061 private static final String CLASS_NAME = "org.opends.server.extensions.PBKDF2PasswordStorageScheme"; 062 063 /** The number of bytes of random data to use as the salt when generating the hashes. */ 064 private static final int NUM_SALT_BYTES = 8; 065 066 /** The number of bytes the SHA-1 algorithm produces. */ 067 private static final int SHA1_LENGTH = 20; 068 069 /** The secure random number generator to use to generate the salt values. */ 070 private SecureRandom random; 071 072 /** The current configuration for this storage scheme. */ 073 private volatile PBKDF2PasswordStorageSchemeCfg config; 074 075 /** 076 * Creates a new instance of this password storage scheme. Note that no 077 * initialization should be performed here, as all initialization should be 078 * done in the <CODE>initializePasswordStorageScheme</CODE> method. 079 */ 080 public PBKDF2PasswordStorageScheme() 081 { 082 super(); 083 } 084 085 /** {@inheritDoc} */ 086 @Override 087 public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration) 088 throws ConfigException, InitializationException 089 { 090 try 091 { 092 random = SecureRandom.getInstance(SECURE_PRNG_SHA1); 093 // Just try to verify if the algorithm is supported. 094 SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); 095 } 096 catch (NoSuchAlgorithmException e) 097 { 098 throw new InitializationException(null); 099 } 100 101 this.config = configuration; 102 config.addPBKDF2ChangeListener(this); 103 } 104 105 /** {@inheritDoc} */ 106 @Override 107 public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration, 108 List<LocalizableMessage> unacceptableReasons) 109 { 110 return true; 111 } 112 113 /** {@inheritDoc} */ 114 @Override 115 public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration) 116 { 117 this.config = configuration; 118 return new ConfigChangeResult(); 119 } 120 121 /** {@inheritDoc} */ 122 @Override 123 public String getStorageSchemeName() 124 { 125 return STORAGE_SCHEME_NAME_PBKDF2; 126 } 127 128 /** {@inheritDoc} */ 129 @Override 130 public ByteString encodePassword(ByteSequence plaintext) 131 throws DirectoryException 132 { 133 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 134 int iterations = config.getPBKDF2Iterations(); 135 136 byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); 137 byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); 138 139 return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt)); 140 } 141 142 /** {@inheritDoc} */ 143 @Override 144 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 145 throws DirectoryException 146 { 147 return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + encodePassword(plaintext)); 148 } 149 150 /** {@inheritDoc} */ 151 @Override 152 public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword) { 153 // Split the iterations from the stored value (separated by a ':') 154 // Base64-decode the remaining value and take the last 8 bytes as the salt. 155 try 156 { 157 final String stored = storedPassword.toString(); 158 final int pos = stored.indexOf(':'); 159 if (pos == -1) 160 { 161 throw new Exception(); 162 } 163 164 final int iterations = Integer.parseInt(stored.substring(0, pos)); 165 byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)); 166 167 final int saltLength = decodedBytes.length - SHA1_LENGTH; 168 if (saltLength <= 0) 169 { 170 logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); 171 return false; 172 } 173 174 final byte[] digestBytes = new byte[SHA1_LENGTH]; 175 final byte[] saltBytes = new byte[saltLength]; 176 System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH); 177 System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength); 178 return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); 179 } 180 catch (Exception e) 181 { 182 logger.traceException(e); 183 logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); 184 return false; 185 } 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 public boolean supportsAuthPasswordSyntax() 191 { 192 return true; 193 } 194 195 /** {@inheritDoc} */ 196 @Override 197 public String getAuthPasswordSchemeName() 198 { 199 return AUTH_PASSWORD_SCHEME_NAME_PBKDF2; 200 } 201 202 /** {@inheritDoc} */ 203 @Override 204 public ByteString encodeAuthPassword(ByteSequence plaintext) 205 throws DirectoryException 206 { 207 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 208 int iterations = config.getPBKDF2Iterations(); 209 byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); 210 211 // Encode and return the value. 212 return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$' 213 + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes)); 214 } 215 216 /** {@inheritDoc} */ 217 @Override 218 public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue) 219 { 220 try 221 { 222 int pos = authInfo.indexOf(':'); 223 if (pos == -1) 224 { 225 throw new Exception(); 226 } 227 int iterations = Integer.parseInt(authInfo.substring(0, pos)); 228 byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1)); 229 byte[] digestBytes = Base64.decode(authValue); 230 return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); 231 } 232 catch (Exception e) 233 { 234 logger.traceException(e); 235 return false; 236 } 237 } 238 239 /** {@inheritDoc} */ 240 @Override 241 public boolean isReversible() 242 { 243 return false; 244 } 245 246 /** {@inheritDoc} */ 247 @Override 248 public ByteString getPlaintextValue(ByteSequence storedPassword) 249 throws DirectoryException 250 { 251 LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2); 252 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 253 } 254 255 /** {@inheritDoc} */ 256 @Override 257 public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue) 258 throws DirectoryException 259 { 260 LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2); 261 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 262 } 263 264 /** {@inheritDoc} */ 265 @Override 266 public boolean isStorageSchemeSecure() 267 { 268 return true; 269 } 270 271 272 /** 273 * Generates an encoded password string from the given clear-text password. 274 * This method is primarily intended for use when it is necessary to generate a password with the server 275 * offline (e.g., when setting the initial root user password). 276 * 277 * @param passwordBytes The bytes that make up the clear-text password. 278 * @return The encoded password string, including the scheme name in curly braces. 279 * @throws DirectoryException If a problem occurs during processing. 280 */ 281 public static String encodeOffline(byte[] passwordBytes) 282 throws DirectoryException 283 { 284 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 285 int iterations = 10000; 286 287 final ByteString password = ByteString.wrap(passwordBytes); 288 byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations); 289 byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); 290 291 return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':' + Base64.encode(hashPlusSalt); 292 } 293 294 private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations) 295 throws DirectoryException 296 { 297 try 298 { 299 final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1); 300 return encodeWithRandomSalt(plaintext, saltBytes, iterations, random); 301 } 302 catch (DirectoryException e) 303 { 304 throw e; 305 } 306 catch (Exception e) 307 { 308 throw cannotEncodePassword(e); 309 } 310 } 311 312 private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) 313 throws DirectoryException 314 { 315 final char[] plaintextChars = plaintext.toString().toCharArray(); 316 try 317 { 318 final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); 319 KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8); 320 return factory.generateSecret(spec).getEncoded(); 321 } 322 catch (Exception e) 323 { 324 throw cannotEncodePassword(e); 325 } 326 finally 327 { 328 Arrays.fill(plaintextChars, '0'); 329 } 330 } 331 332 private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations) 333 { 334 try 335 { 336 final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); 337 return Arrays.equals(digestBytes, userDigestBytes); 338 } 339 catch (Exception e) 340 { 341 return false; 342 } 343 } 344 345 private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, 346 int iterations, SecureRandom random) 347 throws DirectoryException 348 { 349 random.nextBytes(saltBytes); 350 return encodeWithSalt(plaintext, saltBytes, iterations); 351 } 352 353 private static DirectoryException cannotEncodePassword(Exception e) 354 { 355 logger.traceException(e); 356 357 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e)); 358 return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 359 } 360 361 private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) { 362 final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 363 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 364 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, NUM_SALT_BYTES); 365 return hashPlusSalt; 366 } 367 368}