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 2006-2008 Sun Microsystems, Inc. 015 * Portions Copyright 2013-2016 ForgeRock AS. 016 */ 017package org.opends.server.extensions; 018 019import java.security.MessageDigest; 020import java.util.Arrays; 021import java.util.Random; 022 023import org.forgerock.i18n.LocalizableMessage; 024import org.forgerock.opendj.server.config.server.SaltedMD5PasswordStorageSchemeCfg; 025import org.opends.server.api.PasswordStorageScheme; 026import org.forgerock.opendj.config.server.ConfigException; 027import org.opends.server.core.DirectoryServer; 028import org.forgerock.i18n.slf4j.LocalizedLogger; 029import org.opends.server.types.*; 030import org.forgerock.opendj.ldap.ResultCode; 031import org.forgerock.opendj.ldap.ByteString; 032import org.forgerock.opendj.ldap.ByteSequence; 033import org.opends.server.util.Base64; 034 035import static org.opends.messages.ExtensionMessages.*; 036import static org.opends.server.extensions.ExtensionsConstants.*; 037import static org.opends.server.util.StaticUtils.*; 038 039/** 040 * This class defines a Directory Server password storage scheme based on the 041 * MD5 algorithm defined in RFC 1321. This is a one-way digest algorithm so 042 * there is no way to retrieve the original clear-text version of the 043 * password from the hashed value (although this means that it is not suitable 044 * for things that need the clear-text password like DIGEST-MD5). The values 045 * that it generates are also salted, which protects against dictionary attacks. 046 * It does this by generating a 64-bit random salt which is appended to the 047 * clear-text value. A MD5 hash is then generated based on this, the salt is 048 * appended to the hash, and then the entire value is base64-encoded. 049 */ 050public class SaltedMD5PasswordStorageScheme 051 extends PasswordStorageScheme<SaltedMD5PasswordStorageSchemeCfg> 052{ 053 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 054 055 /** The fully-qualified name of this class. */ 056 private static final String CLASS_NAME = 057 "org.opends.server.extensions.SaltedMD5PasswordStorageScheme"; 058 059 /** The number of bytes of random data to use as the salt when generating the hashes. */ 060 private static final int NUM_SALT_BYTES = 8; 061 062 /** The number of bytes MD5 algorithm produces. */ 063 private static final int MD5_LENGTH = 16; 064 065 /** The message digest that will actually be used to generate the MD5 hashes. */ 066 private MessageDigest messageDigest; 067 068 /** The lock used to provide threadsafe access to the message digest. */ 069 private Object digestLock; 070 071 /** The secure random number generator to use to generate the salt values. */ 072 private Random random; 073 074 /** 075 * Creates a new instance of this password storage scheme. Note that no 076 * initialization should be performed here, as all initialization should be 077 * done in the <CODE>initializePasswordStorageScheme</CODE> method. 078 */ 079 public SaltedMD5PasswordStorageScheme() 080 { 081 super(); 082 } 083 084 @Override 085 public void initializePasswordStorageScheme( 086 SaltedMD5PasswordStorageSchemeCfg configuration) 087 throws ConfigException, InitializationException 088 { 089 try 090 { 091 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); 092 } 093 catch (Exception e) 094 { 095 logger.traceException(e); 096 097 LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(MESSAGE_DIGEST_ALGORITHM_MD5, e); 098 throw new InitializationException(message, e); 099 } 100 101 digestLock = new Object(); 102 random = new Random(); 103 } 104 105 @Override 106 public String getStorageSchemeName() 107 { 108 return STORAGE_SCHEME_NAME_SALTED_MD5; 109 } 110 111 @Override 112 public ByteString encodePassword(ByteSequence plaintext) 113 throws DirectoryException 114 { 115 int plainBytesLength = plaintext.length(); 116 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 117 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 118 119 plaintext.copyTo(plainPlusSalt); 120 121 byte[] digestBytes; 122 123 synchronized (digestLock) 124 { 125 try 126 { 127 // Generate the salt and put in the plain+salt array. 128 random.nextBytes(saltBytes); 129 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 130 NUM_SALT_BYTES); 131 132 // Create the hash from the concatenated value. 133 digestBytes = messageDigest.digest(plainPlusSalt); 134 } 135 catch (Exception e) 136 { 137 logger.traceException(e); 138 139 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 140 CLASS_NAME, getExceptionMessage(e)); 141 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 142 message, e); 143 } 144 finally 145 { 146 Arrays.fill(plainPlusSalt, (byte) 0); 147 } 148 } 149 150 // Append the salt to the hashed value and base64-the whole thing. 151 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 152 153 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 154 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 155 NUM_SALT_BYTES); 156 157 return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt)); 158 } 159 160 @Override 161 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 162 throws DirectoryException 163 { 164 StringBuilder buffer = new StringBuilder(); 165 buffer.append('{'); 166 buffer.append(STORAGE_SCHEME_NAME_SALTED_MD5); 167 buffer.append('}'); 168 169 int plainBytesLength = plaintext.length(); 170 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 171 byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES]; 172 173 plaintext.copyTo(plainPlusSalt); 174 175 byte[] digestBytes; 176 177 synchronized (digestLock) 178 { 179 try 180 { 181 // Generate the salt and put in the plain+salt array. 182 random.nextBytes(saltBytes); 183 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, 184 NUM_SALT_BYTES); 185 186 // Create the hash from the concatenated value. 187 digestBytes = messageDigest.digest(plainPlusSalt); 188 } 189 catch (Exception e) 190 { 191 logger.traceException(e); 192 193 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 194 CLASS_NAME, getExceptionMessage(e)); 195 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 196 message, e); 197 } 198 finally 199 { 200 Arrays.fill(plainPlusSalt, (byte) 0); 201 } 202 } 203 204 // Append the salt to the hashed value and base64-the whole thing. 205 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; 206 207 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); 208 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, 209 NUM_SALT_BYTES); 210 buffer.append(Base64.encode(hashPlusSalt)); 211 212 return ByteString.valueOfUtf8(buffer); 213 } 214 215 @Override 216 public boolean passwordMatches(ByteSequence plaintextPassword, 217 ByteSequence storedPassword) 218 { 219 // Base64-decode the stored value and take the last 8 bytes as the salt. 220 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 221 byte[] digestBytes = new byte[MD5_LENGTH]; 222 int saltLength = 0; 223 try 224 { 225 byte[] decodedBytes = Base64.decode(storedPassword.toString()); 226 227 saltLength = decodedBytes.length - MD5_LENGTH; 228 if (saltLength <= 0) 229 { 230 logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); 231 return false; 232 } 233 saltBytes = new byte[saltLength]; 234 System.arraycopy(decodedBytes, 0, digestBytes, 0, MD5_LENGTH); 235 System.arraycopy(decodedBytes, MD5_LENGTH, saltBytes, 0, 236 saltLength); 237 } 238 catch (Exception e) 239 { 240 logger.traceException(e); 241 logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); 242 return false; 243 } 244 245 // Use the salt to generate a digest based on the provided plain-text value. 246 int plainBytesLength = plaintextPassword.length(); 247 byte[] plainPlusSalt = new byte[plainBytesLength + saltLength]; 248 plaintextPassword.copyTo(plainPlusSalt); 249 System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength, 250 saltLength); 251 252 byte[] userDigestBytes; 253 254 synchronized (digestLock) 255 { 256 try 257 { 258 userDigestBytes = messageDigest.digest(plainPlusSalt); 259 } 260 catch (Exception e) 261 { 262 logger.traceException(e); 263 264 return false; 265 } 266 finally 267 { 268 Arrays.fill(plainPlusSalt, (byte) 0); 269 } 270 } 271 272 return Arrays.equals(digestBytes, userDigestBytes); 273 } 274 275 @Override 276 public boolean supportsAuthPasswordSyntax() 277 { 278 // This storage scheme does support the authentication password syntax. 279 return true; 280 } 281 282 @Override 283 public String getAuthPasswordSchemeName() 284 { 285 return AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5; 286 } 287 288 @Override 289 public ByteString encodeAuthPassword(ByteSequence plaintext) 290 throws DirectoryException 291 { 292 int plaintextLength = plaintext.length(); 293 byte[] saltBytes = new byte[NUM_SALT_BYTES]; 294 byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES]; 295 296 plaintext.copyTo(plainPlusSalt); 297 298 byte[] digestBytes; 299 300 synchronized (digestLock) 301 { 302 try 303 { 304 // Generate the salt and put in the plain+salt array. 305 random.nextBytes(saltBytes); 306 System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, 307 NUM_SALT_BYTES); 308 309 // Create the hash from the concatenated value. 310 digestBytes = messageDigest.digest(plainPlusSalt); 311 } 312 catch (Exception e) 313 { 314 logger.traceException(e); 315 316 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 317 CLASS_NAME, getExceptionMessage(e)); 318 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 319 message, e); 320 } 321 finally 322 { 323 Arrays.fill(plainPlusSalt, (byte) 0); 324 } 325 } 326 327 // Encode and return the value. 328 StringBuilder authPWValue = new StringBuilder(); 329 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5); 330 authPWValue.append('$'); 331 authPWValue.append(Base64.encode(saltBytes)); 332 authPWValue.append('$'); 333 authPWValue.append(Base64.encode(digestBytes)); 334 335 return ByteString.valueOfUtf8(authPWValue); 336 } 337 338 @Override 339 public boolean authPasswordMatches(ByteSequence plaintextPassword, 340 String authInfo, String authValue) 341 { 342 byte[] saltBytes; 343 byte[] digestBytes; 344 try 345 { 346 saltBytes = Base64.decode(authInfo); 347 digestBytes = Base64.decode(authValue); 348 } 349 catch (Exception e) 350 { 351 logger.traceException(e); 352 353 return false; 354 } 355 356 int plainBytesLength = plaintextPassword.length(); 357 byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length]; 358 plaintextPassword.copyTo(plainPlusSaltBytes); 359 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, 360 saltBytes.length); 361 362 synchronized (digestLock) 363 { 364 try 365 { 366 return Arrays.equals(digestBytes, 367 messageDigest.digest(plainPlusSaltBytes)); 368 } 369 finally 370 { 371 Arrays.fill(plainPlusSaltBytes, (byte) 0); 372 } 373 } 374 } 375 376 @Override 377 public boolean isReversible() 378 { 379 return false; 380 } 381 382 @Override 383 public ByteString getPlaintextValue(ByteSequence storedPassword) 384 throws DirectoryException 385 { 386 LocalizableMessage message = 387 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_MD5); 388 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 389 } 390 391 @Override 392 public ByteString getAuthPasswordPlaintextValue(String authInfo, 393 String authValue) 394 throws DirectoryException 395 { 396 LocalizableMessage message = 397 ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5); 398 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 399 } 400 401 @Override 402 public boolean isStorageSchemeSecure() 403 { 404 // MD5 may be considered reasonably secure for this purpose. 405 return true; 406 } 407}