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