001/* 002 * or more contributor license agreements. See the NOTICE file 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020 021package org.apache.directory.api.ldap.model.password; 022 023 024import java.security.Key; 025import java.security.MessageDigest; 026import java.security.NoSuchAlgorithmException; 027import java.security.SecureRandom; 028import java.security.spec.KeySpec; 029import java.util.Arrays; 030import java.util.Date; 031 032import javax.crypto.SecretKeyFactory; 033import javax.crypto.spec.PBEKeySpec; 034 035import org.apache.commons.codec.digest.Crypt; 036import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants; 037import org.apache.directory.api.util.Base64; 038import org.apache.directory.api.util.DateUtils; 039import org.apache.directory.api.util.Strings; 040 041/** 042 * A utility class containing methods related to processing passwords. 043 * 044 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 045 */ 046public final class PasswordUtil 047{ 048 049 /** The SHA1 hash length */ 050 public static final int SHA1_LENGTH = 20; 051 052 /** The SHA256 hash length */ 053 public static final int SHA256_LENGTH = 32; 054 055 /** The SHA384 hash length */ 056 public static final int SHA384_LENGTH = 48; 057 058 /** The SHA512 hash length */ 059 public static final int SHA512_LENGTH = 64; 060 061 /** The MD5 hash length */ 062 public static final int MD5_LENGTH = 16; 063 064 /** The PKCS5S2 hash length */ 065 public static final int PKCS5S2_LENGTH = 32; 066 067 /** The CRYPT (DES) hash length */ 068 public static final int CRYPT_LENGTH = 11; 069 070 /** The CRYPT (MD5) hash length */ 071 public static final int CRYPT_MD5_LENGTH = 22; 072 073 /** The CRYPT (SHA-256) hash length */ 074 public static final int CRYPT_SHA256_LENGTH = 43; 075 076 /** The CRYPT (SHA-512) hash length */ 077 public static final int CRYPT_SHA512_LENGTH = 86; 078 079 /** The CRYPT (BCrypt) hash length */ 080 public static final int CRYPT_BCRYPT_LENGTH = 31; 081 082 private static final byte[] CRYPT_SALT_CHARS = Strings 083 .getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ); 084 085 086 private PasswordUtil() 087 { 088 } 089 090 091 /** 092 * Get the algorithm from the stored password. 093 * It can be found on the beginning of the stored password, between 094 * curly brackets. 095 * @param credentials the credentials of the user 096 * @return the name of the algorithm to use 097 */ 098 public static LdapSecurityConstants findAlgorithm( byte[] credentials ) 099 { 100 if ( ( credentials == null ) || ( credentials.length == 0 ) ) 101 { 102 return null; 103 } 104 105 if ( credentials[0] == '{' ) 106 { 107 // get the algorithm 108 int pos = 1; 109 110 while ( pos < credentials.length ) 111 { 112 if ( credentials[pos] == '}' ) 113 { 114 break; 115 } 116 117 pos++; 118 } 119 120 if ( pos < credentials.length ) 121 { 122 if ( pos == 1 ) 123 { 124 // We don't have an algorithm : return the credentials as is 125 return null; 126 } 127 128 String algorithm = Strings.toLowerCaseAscii( Strings.utf8ToString( credentials, 1, pos - 1 ) ); 129 130 // support for crypt additional encryption algorithms (e.g. {crypt}$1$salt$ez2vlPGdaLYkJam5pWs/Y1) 131 if ( credentials.length > pos + 3 && credentials[pos + 1] == '$' 132 && Character.isDigit( credentials[pos + 2] ) ) 133 { 134 if ( credentials[pos + 3] == '$' ) 135 { 136 algorithm += Strings.utf8ToString( credentials, pos + 1, 3 ); 137 } 138 else if ( credentials.length > pos + 4 && credentials[pos + 4] == '$' ) 139 { 140 algorithm += Strings.utf8ToString( credentials, pos + 1, 4 ); 141 } 142 } 143 144 return LdapSecurityConstants.getAlgorithm( algorithm ); 145 } 146 else 147 { 148 // We don't have an algorithm 149 return null; 150 } 151 } 152 else 153 { 154 // No '{algo}' part 155 return null; 156 } 157 } 158 159 160 /** 161 * @see #createStoragePassword(byte[], LdapSecurityConstants) 162 * 163 * @param credentials The password 164 * @param algorithm The algorithm to use 165 * @return The resulting byte[] containing the paswword 166 */ 167 public static byte[] createStoragePassword( String credentials, LdapSecurityConstants algorithm ) 168 { 169 return createStoragePassword( Strings.getBytesUtf8( credentials ), algorithm ); 170 } 171 172 173 /** 174 * create a hashed password in a format that can be stored in the server. 175 * If the specified algorithm requires a salt then a random salt of 8 byte size is used 176 * 177 * @param credentials the plain text password 178 * @param algorithm the hashing algorithm to be applied 179 * @return the password after hashing with the given algorithm 180 */ 181 public static byte[] createStoragePassword( byte[] credentials, LdapSecurityConstants algorithm ) 182 { 183 // check plain text password 184 if ( algorithm == null ) 185 { 186 return credentials; 187 } 188 189 byte[] salt; 190 191 switch ( algorithm ) 192 { 193 case HASH_METHOD_SSHA: 194 case HASH_METHOD_SSHA256: 195 case HASH_METHOD_SSHA384: 196 case HASH_METHOD_SSHA512: 197 case HASH_METHOD_SMD5: 198 // we use 8 byte salt always except for "crypt" which needs 2 byte salt 199 salt = new byte[8]; 200 new SecureRandom().nextBytes( salt ); 201 break; 202 203 case HASH_METHOD_PKCS5S2: 204 // we use 16 byte salt for PKCS5S2 205 salt = new byte[16]; 206 new SecureRandom().nextBytes( salt ); 207 break; 208 209 case HASH_METHOD_CRYPT: 210 salt = generateCryptSalt( 2 ); 211 break; 212 213 case HASH_METHOD_CRYPT_MD5: 214 case HASH_METHOD_CRYPT_SHA256: 215 case HASH_METHOD_CRYPT_SHA512: 216 salt = generateCryptSalt( 8 ); 217 break; 218 219 case HASH_METHOD_CRYPT_BCRYPT: 220 salt = Strings.getBytesUtf8( BCrypt.genSalt() ); 221 break; 222 223 default: 224 salt = null; 225 } 226 227 byte[] hashedPassword = encryptPassword( credentials, algorithm, salt ); 228 StringBuilder sb = new StringBuilder(); 229 230 sb.append( '{' ).append( Strings.upperCase( algorithm.getPrefix() ) ).append( '}' ); 231 232 if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT 233 || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_BCRYPT ) 234 { 235 sb.append( Strings.utf8ToString( salt ) ); 236 sb.append( Strings.utf8ToString( hashedPassword ) ); 237 } 238 else if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_MD5 239 || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA256 240 || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA512 ) 241 { 242 sb.append( algorithm.getSubPrefix() ); 243 sb.append( Strings.utf8ToString( salt ) ); 244 sb.append( '$' ); 245 sb.append( Strings.utf8ToString( hashedPassword ) ); 246 } 247 else if ( salt != null ) 248 { 249 byte[] hashedPasswordWithSaltBytes = new byte[hashedPassword.length + salt.length]; 250 251 if ( algorithm == LdapSecurityConstants.HASH_METHOD_PKCS5S2 ) 252 { 253 merge( hashedPasswordWithSaltBytes, salt, hashedPassword ); 254 } 255 else 256 { 257 merge( hashedPasswordWithSaltBytes, hashedPassword, salt ); 258 } 259 260 sb.append( String.valueOf( Base64.encode( hashedPasswordWithSaltBytes ) ) ); 261 } 262 else 263 { 264 sb.append( String.valueOf( Base64.encode( hashedPassword ) ) ); 265 } 266 267 return Strings.getBytesUtf8( sb.toString() ); 268 } 269 270 271 /** 272 * 273 * Compare the credentials. 274 * We have at least 6 algorithms to encrypt the password : 275 * <ul> 276 * <li>- SHA</li> 277 * <li>- SSHA (salted SHA)</li> 278 * <li>- SHA-2(256, 384 and 512 and their salted versions)</li> 279 * <li>- MD5</li> 280 * <li>- SMD5 (slated MD5)</li> 281 * <li>- PKCS5S2 (PBKDF2)</li> 282 * <li>- crypt (unix crypt)</li> 283 * <li>- plain text, ie no encryption.</li> 284 * </ul> 285 * <p> 286 * If we get an encrypted password, it is prefixed by the used algorithm, between 287 * brackets : {SSHA}password ... 288 * </p> 289 * If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password : 290 * <ul> 291 * <li>- length(password) - 20, starting at 21st position for SSHA</li> 292 * <li>- length(password) - 16, starting at 16th position for SMD5</li> 293 * <li>- length(password) - 2, starting at 3rd position for crypt</li> 294 * </ul> 295 * <p> 296 * For (S)SHA, SHA-256 and (S)MD5, we have to transform the password from Base64 encoded text 297 * to a byte[] before comparing the password with the stored one. 298 * </p> 299 * <p> 300 * For PKCS5S2 the salt is stored in the beginning of the password 301 * </p> 302 * <p> 303 * For crypt, we only have to remove the salt. 304 * </p> 305 * <p> 306 * At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for 307 * the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords. 308 * </p> 309 * <p> 310 * The stored password is always using the unsalted form, and is stored as a bytes array. 311 * </p> 312 * 313 * @param receivedCredentials the credentials provided by user 314 * @param storedCredentials the credentials stored in the server 315 * @return true if they are equal, false otherwise 316 */ 317 public static boolean compareCredentials( byte[] receivedCredentials, byte[] storedCredentials ) 318 { 319 LdapSecurityConstants algorithm = findAlgorithm( storedCredentials ); 320 321 if ( algorithm != null ) 322 { 323 // Let's get the encrypted part of the stored password 324 // We should just keep the password, excluding the algorithm 325 // and the salt, if any. 326 // But we should also get the algorithm and salt to 327 // be able to encrypt the submitted user password in the next step 328 PasswordDetails passwordDetails = PasswordUtil.splitCredentials( storedCredentials ); 329 330 // Reuse the saltedPassword information to construct the encrypted 331 // password given by the user. 332 byte[] userPassword = PasswordUtil.encryptPassword( receivedCredentials, passwordDetails.getAlgorithm(), 333 passwordDetails.getSalt() ); 334 335 return compareBytes( userPassword, passwordDetails.getPassword() ); 336 } 337 else 338 { 339 return compareBytes( receivedCredentials, storedCredentials ); 340 } 341 } 342 343 344 /** 345 * Compare two byte[] in a constant time. This is necessary because using an Array.equals() is 346 * not Timing attack safe ([1], [2] and [3]), a breach that can be exploited to break some hashes. 347 * 348 * [1] https://en.wikipedia.org/wiki/Timing_attack 349 * [2] http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/ 350 * [3] https://cryptocoding.net/index.php/Coding_rules 351 */ 352 private static boolean compareBytes( byte[] provided, byte[] stored ) 353 { 354 if ( stored == null ) 355 { 356 return provided == null; 357 } 358 else if ( provided == null ) 359 { 360 return false; 361 } 362 363 // Now, compare the two passwords, using a constant time method 364 if ( stored.length != provided.length ) 365 { 366 return false; 367 } 368 369 // loop on *every* byte in both passwords, and at the end, if one char at least is different, return false. 370 int result = 0; 371 372 for ( int i = 0; i < stored.length; i++ ) 373 { 374 // If both bytes are equal, xor will be == 0, otherwise it will be != 0 and so will result. 375 result |= ( stored[i] ^ provided[i] ); 376 } 377 378 return result == 0; 379 } 380 381 382 /** 383 * encrypts the given credentials based on the algorithm name and optional salt 384 * 385 * @param credentials the credentials to be encrypted 386 * @param algorithm the algorithm to be used for encrypting the credentials 387 * @param salt value to be used as salt (optional) 388 * @return the encrypted credentials 389 */ 390 private static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt ) 391 { 392 switch ( algorithm ) 393 { 394 case HASH_METHOD_SHA: 395 case HASH_METHOD_SSHA: 396 return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt ); 397 398 case HASH_METHOD_SHA256: 399 case HASH_METHOD_SSHA256: 400 return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt ); 401 402 case HASH_METHOD_SHA384: 403 case HASH_METHOD_SSHA384: 404 return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt ); 405 406 case HASH_METHOD_SHA512: 407 case HASH_METHOD_SSHA512: 408 return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt ); 409 410 case HASH_METHOD_MD5: 411 case HASH_METHOD_SMD5: 412 return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt ); 413 414 case HASH_METHOD_CRYPT: 415 String saltWithCrypted = Crypt.crypt( Strings.utf8ToString( credentials ), Strings 416 .utf8ToString( salt ) ); 417 String crypted = saltWithCrypted.substring( 2 ); 418 return Strings.getBytesUtf8( crypted ); 419 420 case HASH_METHOD_CRYPT_MD5: 421 case HASH_METHOD_CRYPT_SHA256: 422 case HASH_METHOD_CRYPT_SHA512: 423 String saltWithCrypted2 = Crypt.crypt( Strings.utf8ToString( credentials ), 424 algorithm.getSubPrefix() + Strings.utf8ToString( salt ) ); 425 String crypted2 = saltWithCrypted2.substring( saltWithCrypted2.lastIndexOf( '$' ) + 1 ); 426 return Strings.getBytesUtf8( crypted2 ); 427 428 case HASH_METHOD_CRYPT_BCRYPT: 429 String crypted3 = BCrypt.hashPw( Strings.utf8ToString( credentials ), Strings.utf8ToString( salt ) ); 430 return Strings.getBytesUtf8( crypted3.substring( crypted3.length() - 31 ) ); 431 432 case HASH_METHOD_PKCS5S2: 433 return generatePbkdf2Hash( credentials, algorithm, salt ); 434 435 default: 436 return credentials; 437 } 438 } 439 440 441 /** 442 * Compute the hashed password given an algorithm, the credentials and 443 * an optional salt. 444 * 445 * @param algorithm the algorithm to use 446 * @param password the credentials 447 * @param salt the optional salt 448 * @return the digested credentials 449 */ 450 private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt ) 451 { 452 MessageDigest digest; 453 454 try 455 { 456 digest = MessageDigest.getInstance( algorithm.getAlgorithm() ); 457 } 458 catch ( NoSuchAlgorithmException e1 ) 459 { 460 return null; 461 } 462 463 if ( salt != null ) 464 { 465 digest.update( password ); 466 digest.update( salt ); 467 return digest.digest(); 468 } 469 else 470 { 471 return digest.digest( password ); 472 } 473 } 474 475 476 /** 477 * Decompose the stored password in an algorithm, an eventual salt 478 * and the password itself. 479 * 480 * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm 481 * is base64 encoded 482 * 483 * @param credentials The byte[] containing the credentials to split 484 * @return The password 485 */ 486 public static PasswordDetails splitCredentials( byte[] credentials ) 487 { 488 LdapSecurityConstants algorithm = findAlgorithm( credentials ); 489 490 // check plain text password 491 if ( algorithm == null ) 492 { 493 return new PasswordDetails( null, null, credentials ); 494 } 495 496 int algoLength = algorithm.getPrefix().length() + 2; 497 byte[] password; 498 499 switch ( algorithm ) 500 { 501 case HASH_METHOD_MD5: 502 case HASH_METHOD_SMD5: 503 return getCredentials( credentials, algoLength, MD5_LENGTH, algorithm ); 504 505 case HASH_METHOD_SHA: 506 case HASH_METHOD_SSHA: 507 return getCredentials( credentials, algoLength, SHA1_LENGTH, algorithm ); 508 509 case HASH_METHOD_SHA256: 510 case HASH_METHOD_SSHA256: 511 return getCredentials( credentials, algoLength, SHA256_LENGTH, algorithm ); 512 513 case HASH_METHOD_SHA384: 514 case HASH_METHOD_SSHA384: 515 return getCredentials( credentials, algoLength, SHA384_LENGTH, algorithm ); 516 517 case HASH_METHOD_SHA512: 518 case HASH_METHOD_SSHA512: 519 return getCredentials( credentials, algoLength, SHA512_LENGTH, algorithm ); 520 521 case HASH_METHOD_PKCS5S2: 522 return getPbkdf2Credentials( credentials, algoLength, algorithm ); 523 524 case HASH_METHOD_CRYPT: 525 // The password is associated with a salt. Decompose it 526 // in two parts, no decoding required. 527 // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long 528 // The algorithm, salt, and password will be stored into the PasswordDetails structure. 529 byte[] salt = new byte[2]; 530 password = new byte[credentials.length - salt.length - algoLength]; 531 split( credentials, algoLength, salt, password ); 532 return new PasswordDetails( algorithm, salt, password ); 533 534 case HASH_METHOD_CRYPT_BCRYPT: 535 salt = Arrays.copyOfRange( credentials, algoLength, credentials.length - 31 ); 536 password = Arrays.copyOfRange( credentials, credentials.length - 31, credentials.length ); 537 538 return new PasswordDetails( algorithm, salt, password ); 539 case HASH_METHOD_CRYPT_MD5: 540 case HASH_METHOD_CRYPT_SHA256: 541 case HASH_METHOD_CRYPT_SHA512: 542 // skip $x$ 543 algoLength = algoLength + 3; 544 return getCryptCredentials( credentials, algoLength, algorithm ); 545 546 default: 547 // unknown method 548 throw new IllegalArgumentException( "Unknown hash algorithm " + algorithm ); 549 } 550 } 551 552 553 /** 554 * Compute the credentials 555 */ 556 private static PasswordDetails getCredentials( byte[] credentials, int algoLength, int hashLen, 557 LdapSecurityConstants algorithm ) 558 { 559 // The password is associated with a salt. Decompose it 560 // in two parts, after having decoded the password. 561 // The salt is at the end of the credentials. 562 // The algorithm, salt, and password will be stored into the PasswordDetails structure. 563 byte[] passwordAndSalt = Base64 564 .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() ); 565 566 int saltLength = passwordAndSalt.length - hashLen; 567 byte[] salt = saltLength == 0 ? null : new byte[saltLength]; 568 byte[] password = new byte[hashLen]; 569 split( passwordAndSalt, 0, password, salt ); 570 571 return new PasswordDetails( algorithm, salt, password ); 572 } 573 574 575 private static void split( byte[] all, int offset, byte[] left, byte[] right ) 576 { 577 System.arraycopy( all, offset, left, 0, left.length ); 578 if ( right != null ) 579 { 580 System.arraycopy( all, offset + left.length, right, 0, right.length ); 581 } 582 } 583 584 585 private static void merge( byte[] all, byte[] left, byte[] right ) 586 { 587 System.arraycopy( left, 0, all, 0, left.length ); 588 System.arraycopy( right, 0, all, left.length, right.length ); 589 } 590 591 592 /** 593 * checks if the given password's change time is older than the max age 594 * 595 * @param pwdChangedZtime time when the password was last changed 596 * @param pwdMaxAgeSec the max age value in seconds 597 * @return true if expired, false otherwise 598 */ 599 public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec ) 600 { 601 Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime ); 602 603 //DIRSERVER-1735 604 long time = pwdMaxAgeSec * 1000L; 605 time += pwdChangeDate.getTime(); 606 607 Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) ); 608 Date now = DateUtils.getDate( DateUtils.getGeneralizedTime() ); 609 610 boolean expired = false; 611 612 if ( expiryDate.equals( now ) || expiryDate.before( now ) ) 613 { 614 expired = true; 615 } 616 617 return expired; 618 } 619 620 621 /** 622 * generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a> 623 * 624 * Note: this has been implemented to generate hashes compatible with what JIRA generates. 625 * See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a> 626 * @param credentials the credentials 627 * @param algorithm the algorithm to use 628 * @param salt the optional salt 629 * @return the digested credentials 630 */ 631 private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt ) 632 { 633 try 634 { 635 SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() ); 636 char[] password = Strings.utf8ToString( credentials ).toCharArray(); 637 KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 ); 638 Key key = sk.generateSecret( keySpec ); 639 return key.getEncoded(); 640 } 641 catch ( Exception e ) 642 { 643 throw new RuntimeException( e ); 644 } 645 } 646 647 648 /** 649 * Gets the credentials from a PKCS5S2 hash. 650 * The salt for PKCS5S2 hash is prepended to the password 651 */ 652 private static PasswordDetails getPbkdf2Credentials( byte[] credentials, int algoLength, LdapSecurityConstants algorithm ) 653 { 654 // The password is associated with a salt. Decompose it 655 // in two parts, after having decoded the password. 656 // The salt is at the *beginning* of the credentials, and is 16 bytes long 657 // The algorithm, salt, and password will be stored into the PasswordDetails structure. 658 byte[] passwordAndSalt = Base64 659 .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() ); 660 661 int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH; 662 byte[] salt = new byte[saltLength]; 663 byte[] password = new byte[PKCS5S2_LENGTH]; 664 665 split( passwordAndSalt, 0, salt, password ); 666 667 return new PasswordDetails( algorithm, salt, password ); 668 } 669 670 671 private static byte[] generateCryptSalt( int length ) 672 { 673 byte[] salt = new byte[length]; 674 SecureRandom sr = new SecureRandom(); 675 for ( int i = 0; i < salt.length; i++ ) 676 { 677 salt[i] = CRYPT_SALT_CHARS[sr.nextInt( CRYPT_SALT_CHARS.length )]; 678 } 679 680 return salt; 681 } 682 683 684 private static PasswordDetails getCryptCredentials( byte[] credentials, int algoLength, 685 LdapSecurityConstants algorithm ) 686 { 687 // The password is associated with a salt. Decompose it 688 // in two parts, no decoding required. 689 // The salt length is dynamic, between the 2nd and 3rd '$'. 690 // The algorithm, salt, and password will be stored into the PasswordDetails structure. 691 692 // skip {crypt}$x$ 693 int pos = algoLength; 694 while ( pos < credentials.length ) 695 { 696 if ( credentials[pos] == '$' ) 697 { 698 break; 699 } 700 701 pos++; 702 } 703 704 byte[] salt = Arrays.copyOfRange( credentials, algoLength, pos ); 705 byte[] password = Arrays.copyOfRange( credentials, pos + 1, credentials.length ); 706 707 return new PasswordDetails( algorithm, salt, password ); 708 } 709 710}