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