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 2008 Sun Microsystems, Inc. 015 * Portions Copyright 2010-2016 ForgeRock AS. 016 * Portions Copyright 2012 Dariusz Janny <dariusz.janny@gmail.com> 017 */ 018package org.opends.server.extensions; 019 020import java.util.Arrays; 021import java.util.List; 022import java.util.Random; 023 024import org.forgerock.i18n.LocalizableMessage; 025import org.forgerock.opendj.config.server.ConfigurationChangeListener; 026import org.forgerock.opendj.server.config.server.CryptPasswordStorageSchemeCfg; 027import org.forgerock.opendj.server.config.server.PasswordStorageSchemeCfg; 028import org.opends.server.api.PasswordStorageScheme; 029import org.forgerock.opendj.config.server.ConfigChangeResult; 030import org.forgerock.opendj.config.server.ConfigException; 031import org.opends.server.core.DirectoryServer; 032import org.opends.server.types.*; 033import org.forgerock.opendj.ldap.ResultCode; 034import org.forgerock.opendj.ldap.ByteString; 035import org.forgerock.opendj.ldap.ByteSequence; 036import org.opends.server.util.BSDMD5Crypt; 037import org.opends.server.util.Crypt; 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 * UNIX Crypt algorithm. This is a legacy one-way digest algorithm 046 * intended only for situations where passwords have not yet been 047 * updated to modern hashes such as SHA-1 and friends. This 048 * implementation does perform weak salting, which means that it is more 049 * vulnerable to dictionary attacks than schemes with larger salts. 050 */ 051public class CryptPasswordStorageScheme 052 extends PasswordStorageScheme<CryptPasswordStorageSchemeCfg> 053 implements ConfigurationChangeListener<CryptPasswordStorageSchemeCfg> 054{ 055 /** The fully-qualified name of this class for debugging purposes. */ 056 private static final String CLASS_NAME = 057 "org.opends.server.extensions.CryptPasswordStorageScheme"; 058 059 /** The current configuration for the CryptPasswordStorageScheme. */ 060 private CryptPasswordStorageSchemeCfg currentConfig; 061 062 /** 063 * An array of values that can be used to create salt characters 064 * when encoding new crypt hashes. 065 */ 066 private static final byte[] SALT_CHARS = 067 ("./0123456789abcdefghijklmnopqrstuvwxyz" 068 +"ABCDEFGHIJKLMNOPQRSTUVWXYZ").getBytes(); 069 070 private final Random randomSaltIndex = new Random(); 071 private final Object saltLock = new Object(); 072 private final Crypt crypt = new Crypt(); 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 CryptPasswordStorageScheme() 080 { 081 super(); 082 } 083 084 @Override 085 public void initializePasswordStorageScheme( 086 CryptPasswordStorageSchemeCfg configuration) 087 throws ConfigException, InitializationException { 088 configuration.addCryptChangeListener(this); 089 090 currentConfig = configuration; 091 } 092 093 @Override 094 public String getStorageSchemeName() 095 { 096 return STORAGE_SCHEME_NAME_CRYPT; 097 } 098 099 /** Encrypt plaintext password with the Unix Crypt algorithm. */ 100 private ByteString unixCryptEncodePassword(ByteSequence plaintext) 101 throws DirectoryException 102 { 103 byte[] plaintextBytes = null; 104 byte[] digestBytes; 105 106 try 107 { 108 // TODO: can we avoid this copy? 109 plaintextBytes = plaintext.toByteArray(); 110 digestBytes = crypt.crypt(plaintextBytes, randomSalt()); 111 } 112 catch (Exception e) 113 { 114 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 115 CLASS_NAME, stackTraceToSingleLineString(e)); 116 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 117 message, e); 118 } 119 finally 120 { 121 if (plaintextBytes != null) 122 { 123 Arrays.fill(plaintextBytes, (byte) 0); 124 } 125 } 126 127 return ByteString.wrap(digestBytes); 128 } 129 130 /** 131 * Return a random 2-byte salt. 132 * 133 * @return a random 2-byte salt 134 */ 135 private byte[] randomSalt() { 136 synchronized (saltLock) 137 { 138 int sb1 = randomSaltIndex.nextInt(SALT_CHARS.length); 139 int sb2 = randomSaltIndex.nextInt(SALT_CHARS.length); 140 141 return new byte[] { 142 SALT_CHARS[sb1], 143 SALT_CHARS[sb2], 144 }; 145 } 146 } 147 148 private ByteString md5CryptEncodePassword(ByteSequence plaintext) 149 throws DirectoryException 150 { 151 String output; 152 try 153 { 154 output = BSDMD5Crypt.crypt(plaintext); 155 } 156 catch (Exception e) 157 { 158 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 159 CLASS_NAME, stackTraceToSingleLineString(e)); 160 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 161 message, e); 162 } 163 return ByteString.valueOfUtf8(output); 164 } 165 166 private ByteString sha256CryptEncodePassword(ByteSequence plaintext) 167 throws DirectoryException { 168 String output; 169 byte[] plaintextBytes = null; 170 171 try 172 { 173 plaintextBytes = plaintext.toByteArray(); 174 output = Sha2Crypt.sha256Crypt(plaintextBytes); 175 } 176 catch (Exception e) 177 { 178 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 179 CLASS_NAME, stackTraceToSingleLineString(e)); 180 throw new DirectoryException( 181 DirectoryServer.getServerErrorResultCode(), message, e); 182 } 183 finally 184 { 185 if (plaintextBytes != null) 186 { 187 Arrays.fill(plaintextBytes, (byte) 0); 188 } 189 } 190 return ByteString.valueOfUtf8(output); 191 } 192 193 private ByteString sha512CryptEncodePassword(ByteSequence plaintext) 194 throws DirectoryException { 195 String output; 196 byte[] plaintextBytes = null; 197 198 try 199 { 200 plaintextBytes = plaintext.toByteArray(); 201 output = Sha2Crypt.sha512Crypt(plaintextBytes); 202 } 203 catch (Exception e) 204 { 205 LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( 206 CLASS_NAME, stackTraceToSingleLineString(e)); 207 throw new DirectoryException( 208 DirectoryServer.getServerErrorResultCode(), message, e); 209 } 210 finally 211 { 212 if (plaintextBytes != null) 213 { 214 Arrays.fill(plaintextBytes, (byte) 0); 215 } 216 } 217 return ByteString.valueOfUtf8(output); 218 } 219 220 @Override 221 public ByteString encodePassword(ByteSequence plaintext) 222 throws DirectoryException 223 { 224 ByteString bytes = null; 225 switch (currentConfig.getCryptPasswordStorageEncryptionAlgorithm()) 226 { 227 case UNIX: 228 bytes = unixCryptEncodePassword(plaintext); 229 break; 230 case MD5: 231 bytes = md5CryptEncodePassword(plaintext); 232 break; 233 case SHA256: 234 bytes = sha256CryptEncodePassword(plaintext); 235 break; 236 case SHA512: 237 bytes = sha512CryptEncodePassword(plaintext); 238 break; 239 } 240 return bytes; 241 } 242 243 @Override 244 public ByteString encodePasswordWithScheme(ByteSequence plaintext) 245 throws DirectoryException 246 { 247 StringBuilder buffer = 248 new StringBuilder(STORAGE_SCHEME_NAME_CRYPT.length()+12); 249 buffer.append('{'); 250 buffer.append(STORAGE_SCHEME_NAME_CRYPT); 251 buffer.append('}'); 252 253 buffer.append(encodePassword(plaintext)); 254 255 return ByteString.valueOfUtf8(buffer); 256 } 257 258 /** Matches passwords encrypted with the Unix Crypt algorithm. */ 259 private boolean unixCryptPasswordMatches(ByteSequence plaintextPassword, 260 ByteSequence storedPassword) 261 { 262 // TODO: Can we avoid this copy? 263 byte[] plaintextPasswordBytes = null; 264 265 ByteString userPWDigestBytes; 266 try 267 { 268 plaintextPasswordBytes = plaintextPassword.toByteArray(); 269 // The salt is stored as the first two bytes of the storedPassword 270 // value, and crypt.crypt() only looks at the first two bytes, so 271 // we can pass it in directly. 272 byte[] salt = storedPassword.copyTo(new byte[2]); 273 userPWDigestBytes = 274 ByteString.wrap(crypt.crypt(plaintextPasswordBytes, salt)); 275 } 276 catch (Exception e) 277 { 278 return false; 279 } 280 finally 281 { 282 if (plaintextPasswordBytes != null) 283 { 284 Arrays.fill(plaintextPasswordBytes, (byte) 0); 285 } 286 } 287 288 return userPWDigestBytes.equals(storedPassword); 289 } 290 291 private boolean md5CryptPasswordMatches(ByteSequence plaintextPassword, 292 ByteSequence storedPassword) 293 { 294 String storedString = storedPassword.toString(); 295 try 296 { 297 String userString = BSDMD5Crypt.crypt(plaintextPassword, 298 storedString); 299 return userString.equals(storedString); 300 } 301 catch (Exception e) 302 { 303 return false; 304 } 305 } 306 307 private boolean sha256CryptPasswordMatches(ByteSequence plaintextPassword, 308 ByteSequence storedPassword) { 309 byte[] plaintextPasswordBytes = null; 310 String storedString = storedPassword.toString(); 311 try 312 { 313 plaintextPasswordBytes = plaintextPassword.toByteArray(); 314 String userString = Sha2Crypt.sha256Crypt( 315 plaintextPasswordBytes, storedString); 316 return userString.equals(storedString); 317 } 318 catch (Exception e) 319 { 320 return false; 321 } 322 finally 323 { 324 if (plaintextPasswordBytes != null) 325 { 326 Arrays.fill(plaintextPasswordBytes, (byte) 0); 327 } 328 } 329 } 330 331 private boolean sha512CryptPasswordMatches(ByteSequence plaintextPassword, 332 ByteSequence storedPassword) { 333 byte[] plaintextPasswordBytes = null; 334 String storedString = storedPassword.toString(); 335 try 336 { 337 plaintextPasswordBytes = plaintextPassword.toByteArray(); 338 String userString = Sha2Crypt.sha512Crypt( 339 plaintextPasswordBytes, storedString); 340 return userString.equals(storedString); 341 } 342 catch (Exception e) 343 { 344 return false; 345 } 346 finally 347 { 348 if (plaintextPasswordBytes != null) 349 { 350 Arrays.fill(plaintextPasswordBytes, (byte) 0); 351 } 352 } 353 } 354 355 @Override 356 public boolean passwordMatches(ByteSequence plaintextPassword, 357 ByteSequence storedPassword) 358 { 359 String storedString = storedPassword.toString(); 360 if (storedString.startsWith(BSDMD5Crypt.getMagicString())) 361 { 362 return md5CryptPasswordMatches(plaintextPassword, storedPassword); 363 } 364 else if (storedString.startsWith(Sha2Crypt.getMagicSHA256Prefix())) 365 { 366 return sha256CryptPasswordMatches(plaintextPassword, storedPassword); 367 } 368 else if (storedString.startsWith(Sha2Crypt.getMagicSHA512Prefix())) 369 { 370 return sha512CryptPasswordMatches(plaintextPassword, storedPassword); 371 } 372 else 373 { 374 return unixCryptPasswordMatches(plaintextPassword, storedPassword); 375 } 376 } 377 378 @Override 379 public boolean supportsAuthPasswordSyntax() 380 { 381 // This storage scheme does not support the authentication password syntax. 382 return false; 383 } 384 385 @Override 386 public ByteString encodeAuthPassword(ByteSequence plaintext) 387 throws DirectoryException 388 { 389 LocalizableMessage message = 390 ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName()); 391 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 392 } 393 394 @Override 395 public boolean authPasswordMatches(ByteSequence plaintextPassword, 396 String authInfo, String authValue) 397 { 398 // This storage scheme does not support the authentication password syntax. 399 return false; 400 } 401 402 @Override 403 public boolean isReversible() 404 { 405 return false; 406 } 407 408 @Override 409 public ByteString getPlaintextValue(ByteSequence storedPassword) 410 throws DirectoryException 411 { 412 LocalizableMessage message = 413 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_CRYPT); 414 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); 415 } 416 417 @Override 418 public ByteString getAuthPasswordPlaintextValue(String authInfo, 419 String authValue) 420 throws DirectoryException 421 { 422 LocalizableMessage message = 423 ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName()); 424 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 425 } 426 427 @Override 428 public boolean isStorageSchemeSecure() 429 { 430 // FIXME: 431 // Technically, this isn't quite in keeping with the original spirit of 432 // this method, since the point was to determine whether the scheme could 433 // be trivially reversed. I'm not sure I would put crypt into that 434 // category, but it's certainly a lot more vulnerable to lookup tables 435 // than most other algorithms. I'd say we can keep it this way for now, 436 // but it might be something to reconsider later. 437 // Currently, this method is unused. However, the intended purpose is 438 // eventually for use in issue #321, where we could do things like prevent 439 // even authorized users from seeing the password value over an insecure 440 // connection if it isn't considered secure. 441 442 return false; 443 } 444 445 @Override 446 public boolean isConfigurationAcceptable( 447 PasswordStorageSchemeCfg configuration, 448 List<LocalizableMessage> unacceptableReasons) 449 { 450 CryptPasswordStorageSchemeCfg config = 451 (CryptPasswordStorageSchemeCfg) configuration; 452 return isConfigurationChangeAcceptable(config, unacceptableReasons); 453 } 454 455 @Override 456 public boolean isConfigurationChangeAcceptable( 457 CryptPasswordStorageSchemeCfg configuration, 458 List<LocalizableMessage> unacceptableReasons) 459 { 460 // If we've gotten this far, then we'll accept the change. 461 return true; 462 } 463 464 @Override 465 public ConfigChangeResult applyConfigurationChange( 466 CryptPasswordStorageSchemeCfg configuration) 467 { 468 currentConfig = configuration; 469 return new ConfigChangeResult(); 470 } 471}