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