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