View Javadoc
1   /*
2    *   or more contributor license agreements.  See the NOTICE file
3    *   Licensed to the Apache Software Foundation (ASF) under one
4    *   distributed with this work for additional information
5    *   regarding copyright ownership.  The ASF licenses this file
6    *   to you under the Apache License, Version 2.0 (the
7    *   "License"); you may not use this file except in compliance
8    *   with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *   Unless required by applicable law or agreed to in writing,
13   *   software distributed under the License is distributed on an
14   *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *   KIND, either express or implied.  See the License for the
16   *   specific language governing permissions and limitations
17   *   under the License.
18   *
19   */
20  
21  package org.apache.directory.api.ldap.model.password;
22  
23  
24  import java.io.UnsupportedEncodingException;
25  import java.security.Key;
26  import java.security.MessageDigest;
27  import java.security.NoSuchAlgorithmException;
28  import java.security.SecureRandom;
29  import java.security.spec.KeySpec;
30  import java.util.Arrays;
31  import java.util.Date;
32  
33  import javax.crypto.SecretKeyFactory;
34  import javax.crypto.spec.PBEKeySpec;
35  
36  import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
37  import org.apache.directory.api.util.Base64;
38  import org.apache.directory.api.util.DateUtils;
39  import org.apache.directory.api.util.Strings;
40  import org.apache.directory.api.util.UnixCrypt;
41  
42  
43  /**
44   * A utility class containing methods related to processing passwords.
45   *
46   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
47   */
48  public class PasswordUtil
49  {
50  
51      /** The SHA1 hash length */
52      public static final int SHA1_LENGTH = 20;
53  
54      /** The SHA256 hash length */
55      public static final int SHA256_LENGTH = 32;
56  
57      /** The SHA384 hash length */
58      public static final int SHA384_LENGTH = 48;
59  
60      /** The SHA512 hash length */
61      public static final int SHA512_LENGTH = 64;
62  
63      /** The MD5 hash length */
64      public static final int MD5_LENGTH = 16;
65  
66      /** The PKCS5S2 hash length */
67      public static final int PKCS5S2_LENGTH = 32;
68  
69      /**
70       * Get the algorithm from the stored password. 
71       * It can be found on the beginning of the stored password, between 
72       * curly brackets.
73       * @param credentials the credentials of the user
74       * @return the name of the algorithm to use
75       */
76      public static LdapSecurityConstants findAlgorithm( byte[] credentials )
77      {
78          if ( ( credentials == null ) || ( credentials.length == 0 ) )
79          {
80              return null;
81          }
82  
83          if ( credentials[0] == '{' )
84          {
85              // get the algorithm
86              int pos = 1;
87  
88              while ( pos < credentials.length )
89              {
90                  if ( credentials[pos] == '}' )
91                  {
92                      break;
93                  }
94  
95                  pos++;
96              }
97  
98              if ( pos < credentials.length )
99              {
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 }