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-2016 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.forgerock.opendj.config.server.ConfigurationChangeListener; 034import org.forgerock.opendj.server.config.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 @Override 086 public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration) 087 throws ConfigException, InitializationException 088 { 089 try 090 { 091 random = SecureRandom.getInstance(SECURE_PRNG_SHA1); 092 // Just try to verify if the algorithm is supported. 093 SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); 094 } 095 catch (NoSuchAlgorithmException e) 096 { 097 throw new InitializationException(null); 098 } 099 100 this.config = configuration; 101 config.addPBKDF2ChangeListener(this); 102 } 103 104 @Override 105 public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration, 106 List<LocalizableMessage> unacceptableReasons) 107 { 108 return true; 109 } 110 111 @Override 112 public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration) 113 { 114 this.config = configuration; 115 return new ConfigChangeResult(); 116 } 117 118 @Override 119 public String getStorageSchemeName() 120 { 121 return STORAGE_SCHEME_NAME_PBKDF2; 122 } 123 124 @Override 125 public ByteString encodePassword(ByteSequence plaintext) 126 throws DirectoryException 127 { 128 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 129 int iterations = config.getPBKDF2Iterations(); 130 131 byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); 132 byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); 133 134 return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt)); 135 } 136 137 @Override 138 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 139 throws DirectoryException 140 { 141 return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + encodePassword(plaintext)); 142 } 143 144 @Override 145 public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword) { 146 // Split the iterations from the stored value (separated by a ':') 147 // Base64-decode the remaining value and take the last 8 bytes as the salt. 148 try 149 { 150 final String stored = storedPassword.toString(); 151 final int pos = stored.indexOf(':'); 152 if (pos == -1) 153 { 154 throw new Exception(); 155 } 156 157 final int iterations = Integer.parseInt(stored.substring(0, pos)); 158 byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)); 159 160 final int saltLength = decodedBytes.length - SHA1_LENGTH; 161 if (saltLength <= 0) 162 { 163 logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); 164 return false; 165 } 166 167 final byte[] digestBytes = new byte[SHA1_LENGTH]; 168 final byte[] saltBytes = new byte[saltLength]; 169 System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH); 170 System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength); 171 return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); 172 } 173 catch (Exception e) 174 { 175 logger.traceException(e); 176 logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); 177 return false; 178 } 179 } 180 181 @Override 182 public boolean supportsAuthPasswordSyntax() 183 { 184 return true; 185 } 186 187 @Override 188 public String getAuthPasswordSchemeName() 189 { 190 return AUTH_PASSWORD_SCHEME_NAME_PBKDF2; 191 } 192 193 @Override 194 public ByteString encodeAuthPassword(ByteSequence plaintext) 195 throws DirectoryException 196 { 197 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 198 int iterations = config.getPBKDF2Iterations(); 199 byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); 200 201 // Encode and return the value. 202 return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$' 203 + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes)); 204 } 205 206 @Override 207 public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue) 208 { 209 try 210 { 211 int pos = authInfo.indexOf(':'); 212 if (pos == -1) 213 { 214 throw new Exception(); 215 } 216 int iterations = Integer.parseInt(authInfo.substring(0, pos)); 217 byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1)); 218 byte[] digestBytes = Base64.decode(authValue); 219 return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); 220 } 221 catch (Exception e) 222 { 223 logger.traceException(e); 224 return false; 225 } 226 } 227 228 @Override 229 public boolean isReversible() 230 { 231 return false; 232 } 233 234 @Override 235 public ByteString getPlaintextValue(ByteSequence storedPassword) 236 throws DirectoryException 237 { 238 LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2); 239 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 240 } 241 242 @Override 243 public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue) 244 throws DirectoryException 245 { 246 LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2); 247 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 248 } 249 250 @Override 251 public boolean isStorageSchemeSecure() 252 { 253 return true; 254 } 255 256 /** 257 * Generates an encoded password string from the given clear-text password. 258 * This method is primarily intended for use when it is necessary to generate a password with the server 259 * offline (e.g., when setting the initial root user password). 260 * 261 * @param passwordBytes The bytes that make up the clear-text password. 262 * @return The encoded password string, including the scheme name in curly braces. 263 * @throws DirectoryException If a problem occurs during processing. 264 */ 265 public static String encodeOffline(byte[] passwordBytes) 266 throws DirectoryException 267 { 268 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 269 int iterations = 10000; 270 271 final ByteString password = ByteString.wrap(passwordBytes); 272 byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations); 273 byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); 274 275 return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':' + Base64.encode(hashPlusSalt); 276 } 277 278 private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations) 279 throws DirectoryException 280 { 281 try 282 { 283 final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1); 284 return encodeWithRandomSalt(plaintext, saltBytes, iterations, random); 285 } 286 catch (DirectoryException e) 287 { 288 throw e; 289 } 290 catch (Exception e) 291 { 292 throw cannotEncodePassword(e); 293 } 294 } 295 296 private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) 297 throws DirectoryException 298 { 299 final char[] plaintextChars = plaintext.toString().toCharArray(); 300 try 301 { 302 final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); 303 KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8); 304 return factory.generateSecret(spec).getEncoded(); 305 } 306 catch (Exception e) 307 { 308 throw cannotEncodePassword(e); 309 } 310 finally 311 { 312 Arrays.fill(plaintextChars, '0'); 313 } 314 } 315 316 private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations) 317 { 318 try 319 { 320 final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); 321 return Arrays.equals(digestBytes, userDigestBytes); 322 } 323 catch (Exception e) 324 { 325 return false; 326 } 327 } 328 329 private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, 330 int iterations, SecureRandom random) 331 throws DirectoryException 332 { 333 random.nextBytes(saltBytes); 334 return encodeWithSalt(plaintext, saltBytes, iterations); 335 } 336 337 private static DirectoryException cannotEncodePassword(Exception e) 338 { 339 logger.traceException(e); 340 341 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e)); 342 return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 343 } 344 345 private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) { 346 final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 347 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 348 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, NUM_SALT_BYTES); 349 return hashPlusSalt; 350 } 351}