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