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.io.UnsupportedEncodingException; 025import java.security.Key; 026import java.security.MessageDigest; 027import java.security.NoSuchAlgorithmException; 028import java.security.SecureRandom; 029import java.security.spec.KeySpec; 030import java.util.Arrays; 031import java.util.Date; 032 033import javax.crypto.SecretKeyFactory; 034import javax.crypto.spec.PBEKeySpec; 035 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; 040import org.apache.directory.api.util.UnixCrypt; 041 042 043/** 044 * A utility class containing methods related to processing passwords. 045 * 046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 047 */ 048public class PasswordUtil 049{ 050 051 /** The SHA1 hash length */ 052 public static final int SHA1_LENGTH = 20; 053 054 /** The SHA256 hash length */ 055 public static final int SHA256_LENGTH = 32; 056 057 /** The SHA384 hash length */ 058 public static final int SHA384_LENGTH = 48; 059 060 /** The SHA512 hash length */ 061 public static final int SHA512_LENGTH = 64; 062 063 /** The MD5 hash length */ 064 public static final int MD5_LENGTH = 16; 065 066 /** The PKCS5S2 hash length */ 067 public static final int PKCS5S2_LENGTH = 32; 068 069 /** 070 * Get the algorithm from the stored password. 071 * It can be found on the beginning of the stored password, between 072 * curly brackets. 073 * @param credentials the credentials of the user 074 * @return the name of the algorithm to use 075 */ 076 public static LdapSecurityConstants findAlgorithm( byte[] credentials ) 077 { 078 if ( ( credentials == null ) || ( credentials.length == 0 ) ) 079 { 080 return null; 081 } 082 083 if ( credentials[0] == '{' ) 084 { 085 // get the algorithm 086 int pos = 1; 087 088 while ( pos < credentials.length ) 089 { 090 if ( credentials[pos] == '}' ) 091 { 092 break; 093 } 094 095 pos++; 096 } 097 098 if ( pos < credentials.length ) 099 { 100 if ( pos == 1 ) 101 { 102 // We don't have an algorithm : return the credentials as is 103 return null; 104 } 105 106 String algorithm = Strings.toLowerCase( new String( credentials, 1, pos - 1 ) ); 107 108 return LdapSecurityConstants.getAlgorithm( algorithm ); 109 } 110 else 111 { 112 // We don't have an algorithm 113 return null; 114 } 115 } 116 else 117 { 118 // No '{algo}' part 119 return null; 120 } 121 } 122 123 124 /** 125 * @see #createStoragePassword(byte[], LdapSecurityConstants) 126 */ 127 public static byte[] createStoragePassword( String credentials, LdapSecurityConstants algorithm ) 128 { 129 return createStoragePassword( Strings.getBytesUtf8( credentials ), algorithm ); 130 } 131 132 133 /** 134 * create a hashed password in a format that can be stored in the server. 135 * If the specified algorithm requires a salt then a random salt of 8 byte size is used 136 * 137 * @param credentials the plain text password 138 * @param algorithm the hashing algorithm to be applied 139 * @return the password after hashing with the given algorithm 140 */ 141 public static byte[] createStoragePassword( byte[] credentials, LdapSecurityConstants algorithm ) 142 { 143 byte[] salt; 144 145 switch ( algorithm ) 146 { 147 case HASH_METHOD_SSHA: 148 case HASH_METHOD_SSHA256: 149 case HASH_METHOD_SSHA384: 150 case HASH_METHOD_SSHA512: 151 case HASH_METHOD_SMD5: 152 salt = new byte[8]; // we use 8 byte salt always except for "crypt" which needs 2 byte salt 153 new SecureRandom().nextBytes( salt ); 154 break; 155 156 case HASH_METHOD_PKCS5S2: 157 salt = new byte[16]; // we use 16 byte salt for PKCS5S2 158 new SecureRandom().nextBytes( salt ); 159 break; 160 161 case HASH_METHOD_CRYPT: 162 salt = new byte[2]; 163 SecureRandom sr = new SecureRandom(); 164 int i1 = sr.nextInt( 64 ); 165 int i2 = sr.nextInt( 64 ); 166 167 salt[0] = ( byte ) ( i1 < 12 ? ( i1 + '.' ) : i1 < 38 ? ( i1 + 'A' - 12 ) : ( i1 + 'a' - 38 ) ); 168 salt[1] = ( byte ) ( i2 < 12 ? ( i2 + '.' ) : i2 < 38 ? ( i2 + 'A' - 12 ) : ( i2 + 'a' - 38 ) ); 169 break; 170 171 default: 172 salt = null; 173 } 174 175 byte[] hashedPassword = encryptPassword( credentials, algorithm, salt ); 176 StringBuffer sb = new StringBuffer(); 177 178 if ( algorithm != null ) 179 { 180 sb.append( '{' ).append( algorithm.getPrefix().toUpperCase() ).append( '}' ); 181 182 if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT ) 183 { 184 sb.append( Strings.utf8ToString( salt ) ); 185 sb.append( Strings.utf8ToString( hashedPassword ) ); 186 } 187 else if ( salt != null ) 188 { 189 byte[] hashedPasswordWithSaltBytes = new byte[hashedPassword.length + salt.length]; 190 191 if ( algorithm == LdapSecurityConstants.HASH_METHOD_PKCS5S2 ) 192 { 193 merge( hashedPasswordWithSaltBytes, salt, hashedPassword ); 194 } 195 else 196 { 197 merge( hashedPasswordWithSaltBytes, hashedPassword, salt ); 198 } 199 200 sb.append( String.valueOf( Base64.encode( hashedPasswordWithSaltBytes ) ) ); 201 } 202 else 203 { 204 sb.append( String.valueOf( Base64.encode( hashedPassword ) ) ); 205 } 206 } 207 else 208 { 209 sb.append( Strings.utf8ToString( hashedPassword ) ); 210 } 211 212 return Strings.getBytesUtf8( sb.toString() ); 213 } 214 215 216 /** 217 * 218 * Compare the credentials. 219 * We have at least 6 algorithms to encrypt the password : 220 * <ul> 221 * <li>- SHA</li> 222 * <li>- SSHA (salted SHA)</li> 223 * <li>- SHA-2(256, 384 and 512 and their salted versions)</li> 224 * <li>- MD5</li> 225 * <li>- SMD5 (slated MD5)</li> 226 * <li>- PKCS5S2 (PBKDF2)</li> 227 * <li>- crypt (unix crypt)</li> 228 * <li>- plain text, ie no encryption.</li> 229 * </ul> 230 * <p> 231 * If we get an encrypted password, it is prefixed by the used algorithm, between 232 * brackets : {SSHA}password ... 233 * </p> 234 * If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password : 235 * <ul> 236 * <li>- length(password) - 20, starting at 21st position for SSHA</li> 237 * <li>- length(password) - 16, starting at 16th position for SMD5</li> 238 * <li>- length(password) - 2, starting at 3rd position for crypt</li> 239 * </ul> 240 * <p> 241 * For (S)SHA, SHA-256 and (S)MD5, we have to transform the password from Base64 encoded text 242 * to a byte[] before comparing the password with the stored one. 243 * </p> 244 * <p> 245 * For PKCS5S2 the salt is stored in the beginning of the password 246 * </p> 247 * <p> 248 * For crypt, we only have to remove the salt. 249 * </p> 250 * <p> 251 * At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for 252 * the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords. 253 * </p> 254 * <p> 255 * The stored password is always using the unsalted form, and is stored as a bytes array. 256 * </p> 257 * 258 * @param receivedCredentials the credentials provided by user 259 * @param storedCredentials the credentials stored in the server 260 * @return true if they are equal, false otherwise 261 */ 262 public static boolean compareCredentials( byte[] receivedCredentials, byte[] storedCredentials ) 263 { 264 LdapSecurityConstants algorithm = findAlgorithm( storedCredentials ); 265 266 if ( algorithm != null ) 267 { 268 EncryptionMethod encryptionMethod = new EncryptionMethod( algorithm, null ); 269 270 // Let's get the encrypted part of the stored password 271 // We should just keep the password, excluding the algorithm 272 // and the salt, if any. 273 // But we should also get the algorithm and salt to 274 // be able to encrypt the submitted user password in the next step 275 byte[] encryptedStored = PasswordUtil.splitCredentials( storedCredentials, encryptionMethod ); 276 277 // Reuse the saltedPassword informations to construct the encrypted 278 // password given by the user. 279 byte[] userPassword = PasswordUtil.encryptPassword( receivedCredentials, encryptionMethod.getAlgorithm(), 280 encryptionMethod.getSalt() ); 281 282 // Now, compare the two passwords. 283 return Arrays.equals( userPassword, encryptedStored ); 284 } 285 else 286 { 287 return Arrays.equals( storedCredentials, receivedCredentials ); 288 } 289 } 290 291 292 /** 293 * encrypts the given credentials based on the algorithm name and optional salt 294 * 295 * @param credentials the credentials to be encrypted 296 * @param algorithm the algorithm to be used for encrypting the credentials 297 * @param salt value to be used as salt (optional) 298 * @return the encrypted credentials 299 */ 300 public static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt ) 301 { 302 switch ( algorithm ) 303 { 304 case HASH_METHOD_SHA: 305 case HASH_METHOD_SSHA: 306 return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt ); 307 308 case HASH_METHOD_SHA256: 309 case HASH_METHOD_SSHA256: 310 return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt ); 311 312 case HASH_METHOD_SHA384: 313 case HASH_METHOD_SSHA384: 314 return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt ); 315 316 case HASH_METHOD_SHA512: 317 case HASH_METHOD_SSHA512: 318 return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt ); 319 320 case HASH_METHOD_MD5: 321 case HASH_METHOD_SMD5: 322 return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt ); 323 324 case HASH_METHOD_CRYPT: 325 String saltWithCrypted = UnixCrypt.crypt( Strings.utf8ToString( credentials ), Strings 326 .utf8ToString( salt ) ); 327 String crypted = saltWithCrypted.substring( 2 ); 328 329 return Strings.getBytesUtf8( crypted ); 330 331 case HASH_METHOD_PKCS5S2: 332 return generatePbkdf2Hash( credentials, algorithm, salt ); 333 334 default: 335 return credentials; 336 } 337 } 338 339 340 /** 341 * Compute the hashed password given an algorithm, the credentials and 342 * an optional salt. 343 * 344 * @param algorithm the algorithm to use 345 * @param password the credentials 346 * @param salt the optional salt 347 * @return the digested credentials 348 */ 349 private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt ) 350 { 351 MessageDigest digest; 352 353 try 354 { 355 digest = MessageDigest.getInstance( algorithm.getAlgorithm() ); 356 } 357 catch ( NoSuchAlgorithmException e1 ) 358 { 359 return null; 360 } 361 362 if ( salt != null ) 363 { 364 digest.update( password ); 365 digest.update( salt ); 366 return digest.digest(); 367 } 368 else 369 { 370 return digest.digest( password ); 371 } 372 } 373 374 375 /** 376 * Decompose the stored password in an algorithm, an eventual salt 377 * and the password itself. 378 * 379 * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm 380 * is base64 encoded 381 * 382 * @param encryptionMethod The structure to feed 383 * @return The password 384 * @param credentials the credentials to split 385 */ 386 public static byte[] splitCredentials( byte[] credentials, EncryptionMethod encryptionMethod ) 387 { 388 int algoLength = encryptionMethod.getAlgorithm().getPrefix().length() + 2; 389 390 switch ( encryptionMethod.getAlgorithm() ) 391 { 392 case HASH_METHOD_MD5: 393 case HASH_METHOD_SHA: 394 try 395 { 396 // We just have the password just after the algorithm, base64 encoded. 397 // Just decode the password and return it. 398 return Base64 399 .decode( new String( credentials, algoLength, credentials.length - algoLength, "UTF-8" ) 400 .toCharArray() ); 401 } 402 catch ( UnsupportedEncodingException uee ) 403 { 404 // do nothing 405 return credentials; 406 } 407 408 case HASH_METHOD_SMD5: 409 try 410 { 411 // The password is associated with a salt. Decompose it 412 // in two parts, after having decoded the password. 413 // The salt will be stored into the EncryptionMethod structure 414 // The salt is at the end of the credentials, and is 8 bytes long 415 byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length 416 - algoLength, "UTF-8" ).toCharArray() ); 417 418 int saltLength = passwordAndSalt.length - MD5_LENGTH; 419 encryptionMethod.setSalt( new byte[saltLength] ); 420 byte[] password = new byte[MD5_LENGTH]; 421 split( passwordAndSalt, 0, password, encryptionMethod.getSalt() ); 422 423 return password; 424 } 425 catch ( UnsupportedEncodingException uee ) 426 { 427 // do nothing 428 return credentials; 429 } 430 431 case HASH_METHOD_SSHA: 432 return getCredentials( credentials, algoLength, SHA1_LENGTH, encryptionMethod ); 433 434 case HASH_METHOD_SHA256: 435 case HASH_METHOD_SSHA256: 436 return getCredentials( credentials, algoLength, SHA256_LENGTH, encryptionMethod ); 437 438 case HASH_METHOD_SHA384: 439 case HASH_METHOD_SSHA384: 440 return getCredentials( credentials, algoLength, SHA384_LENGTH, encryptionMethod ); 441 442 case HASH_METHOD_SHA512: 443 case HASH_METHOD_SSHA512: 444 return getCredentials( credentials, algoLength, SHA512_LENGTH, encryptionMethod ); 445 446 case HASH_METHOD_PKCS5S2: 447 return getPbkdf2Credentials( credentials, algoLength, encryptionMethod ); 448 449 case HASH_METHOD_CRYPT: 450 // The password is associated with a salt. Decompose it 451 // in two parts, storing the salt into the EncryptionMethod structure. 452 // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long 453 encryptionMethod.setSalt( new byte[2] ); 454 byte[] password = new byte[credentials.length - encryptionMethod.getSalt().length - algoLength]; 455 split( credentials, algoLength, encryptionMethod.getSalt(), password ); 456 457 return password; 458 459 default: 460 // unknown method 461 return credentials; 462 463 } 464 } 465 466 467 /** 468 * Compute the credentials 469 */ 470 private static byte[] getCredentials( byte[] credentials, int algoLength, int hashLen, 471 EncryptionMethod encryptionMethod ) 472 { 473 try 474 { 475 // The password is associated with a salt. Decompose it 476 // in two parts, after having decoded the password. 477 // The salt will be stored into the EncryptionMethod structure 478 // The salt is at the end of the credentials, and is 8 bytes long 479 byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length 480 - algoLength, "UTF-8" ).toCharArray() ); 481 482 int saltLength = passwordAndSalt.length - hashLen; 483 encryptionMethod.setSalt( new byte[saltLength] ); 484 byte[] password = new byte[hashLen]; 485 split( passwordAndSalt, 0, password, encryptionMethod.getSalt() ); 486 487 return password; 488 } 489 catch ( UnsupportedEncodingException uee ) 490 { 491 // do nothing 492 return credentials; 493 } 494 } 495 496 497 private static void split( byte[] all, int offset, byte[] left, byte[] right ) 498 { 499 System.arraycopy( all, offset, left, 0, left.length ); 500 System.arraycopy( all, offset + left.length, right, 0, right.length ); 501 } 502 503 504 private static void merge( byte[] all, byte[] left, byte[] right ) 505 { 506 System.arraycopy( left, 0, all, 0, left.length ); 507 System.arraycopy( right, 0, all, left.length, right.length ); 508 } 509 510 511 /** 512 * checks if the given password's change time is older than the max age 513 * 514 * @param pwdChangedZtime time when the password was last changed 515 * @param pwdMaxAgeSec the max age value in seconds 516 * @return true if expired, false otherwise 517 */ 518 public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec ) 519 { 520 Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime ); 521 522 long time = pwdMaxAgeSec * 1000L;//DIRSERVER-1735 523 time += pwdChangeDate.getTime(); 524 525 Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) ); 526 Date now = DateUtils.getDate( DateUtils.getGeneralizedTime() ); 527 528 boolean expired = false; 529 530 if ( expiryDate.equals( now ) || expiryDate.before( now ) ) 531 { 532 expired = true; 533 } 534 535 return expired; 536 } 537 538 539 /** 540 * generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a> 541 * 542 * Note: this has been implemented to generate hashes compatible with what JIRA generates. 543 * See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a> 544 * @param algorithm the algorithm to use 545 * @param password the credentials 546 * @param salt the optional salt 547 * @return the digested credentials 548 */ 549 private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt ) 550 { 551 try 552 { 553 SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() ); 554 char[] password = Strings.utf8ToString( credentials ).toCharArray(); 555 KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 ); 556 Key key = sk.generateSecret( keySpec ); 557 return key.getEncoded(); 558 } 559 catch( Exception e ) 560 { 561 throw new RuntimeException( e ); 562 } 563 } 564 565 566 /** 567 * Gets the credentials from a PKCS5S2 hash. 568 * The salt for PKCS5S2 hash is prepended to the password 569 */ 570 private static byte[] getPbkdf2Credentials( byte[] credentials, int algoLength, EncryptionMethod encryptionMethod ) 571 { 572 try 573 { 574 // The password is associated with a salt. Decompose it 575 // in two parts, after having decoded the password. 576 // The salt will be stored into the EncryptionMethod structure 577 // The salt is at the *beginning* of the credentials, and is 16 bytes long 578 byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length 579 - algoLength, "UTF-8" ).toCharArray() ); 580 581 int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH; 582 encryptionMethod.setSalt( new byte[saltLength] ); 583 byte[] password = new byte[PKCS5S2_LENGTH]; 584 585 split( passwordAndSalt, 0, encryptionMethod.getSalt(), password ); 586 587 return password; 588 } 589 catch ( UnsupportedEncodingException uee ) 590 { 591 // do nothing 592 return credentials; 593 } 594 } 595 596}