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