View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.business.user.authentication;
35  
36  import java.security.MessageDigest;
37  import java.security.NoSuchAlgorithmException;
38  import java.security.SecureRandom;
39  import java.security.spec.InvalidKeySpecException;
40  import java.util.Arrays;
41  import java.util.Random;
42  import java.util.regex.Matcher;
43  import java.util.regex.Pattern;
44  
45  import javax.crypto.SecretKeyFactory;
46  import javax.crypto.spec.PBEKeySpec;
47  
48  import org.apache.commons.codec.DecoderException;
49  import org.apache.commons.codec.binary.Hex;
50  
51  import fr.paris.lutece.portal.service.util.AppException;
52  import fr.paris.lutece.portal.service.util.AppLogService;
53  import fr.paris.lutece.portal.service.util.AppPropertiesService;
54  import fr.paris.lutece.portal.service.util.CryptoService;
55  import fr.paris.lutece.util.password.IPassword;
56  import fr.paris.lutece.util.password.IPasswordFactory;
57  
58  /**
59   * A factory for getting storable password representation
60   */
61  final class PasswordFactory implements IPasswordFactory
62  {
63      // storage types
64      private static final String ERROR_PASSWORD_STORAGE = "Invalid stored password ";
65      private static final String PBKDF2WITHHMACSHA1_STORAGE_TYPE = "PBKDF2";
66      private static final String PBKDF2WITHHMACSHA512_STORAGE_TYPE = "PBKDF2WITHHMACSHA512";
67      private static final String PLAINTEXT_STORAGE_TYPE = "PLAINTEXT";
68      private static final String DUMMY_STORAGE_TYPE = "\0DUMMY\0";
69      private static final String DUMMY_STORED_PASSWORD = DUMMY_STORAGE_TYPE + ":\0";
70  
71      @Override
72      public IPassword getPassword( String strStoredPassword )
73      {
74          int storageTypeSeparatorIndex = strStoredPassword.indexOf( ':' );
75          if ( storageTypeSeparatorIndex == -1 )
76          {
77              throw new IllegalArgumentException( strStoredPassword );
78          }
79          String storageType = strStoredPassword.substring( 0, storageTypeSeparatorIndex );
80          String password = strStoredPassword.substring( storageTypeSeparatorIndex + 1 );
81          switch( storageType )
82          {
83              case PLAINTEXT_STORAGE_TYPE:
84                  return new PlaintextPassword( password );
85              case PBKDF2WITHHMACSHA1_STORAGE_TYPE:
86                  return new PBKDF2WithHmacSHA1Password( password );
87              case PBKDF2WITHHMACSHA512_STORAGE_TYPE:
88                  return new PBKDF2WithHmacSHA512Password( password );
89              case DUMMY_STORAGE_TYPE:
90                  return new DummyPassword( );
91              default:
92                  return new DigestPassword( storageType, password );
93          }
94      }
95  
96      @Override
97      public IPassword getPasswordFromCleartext( String strUserPassword )
98      {
99          return new PBKDF2WithHmacSHA512Password( strUserPassword, PBKDF2Password.PASSWORD_REPRESENTATION.CLEARTEXT );
100     }
101 
102     @Override
103     public IPassword getDummyPassword( )
104     {
105         return getPassword( DUMMY_STORED_PASSWORD );
106     }
107 
108     /**
109      * A Password stored using PBKDF2
110      */
111     private abstract static class PBKDF2Password implements IPassword
112     {
113 
114         /**
115          * Enum to specify if the password is constructed from cleartext or hashed form
116          */
117         enum PASSWORD_REPRESENTATION
118         {
119             CLEARTEXT,
120             STORABLE
121         }
122 
123         /** Storage format : iterations:hex(salt):hex(hash) */
124         private static final Pattern FORMAT = Pattern.compile( "^(\\d+):([a-z0-9]+):([a-z0-9]+)$", Pattern.CASE_INSENSITIVE );
125         private static final Random RANDOM;
126 
127         // init the random number generator
128         static
129         {
130             Random rand;
131             try
132             {
133                 rand = SecureRandom.getInstance( "SHA1PRNG" );
134             }
135             catch( NoSuchAlgorithmException e )
136             {
137                 AppLogService.error( "SHA1PRNG is not availabled. Picking the default SecureRandom.", e );
138                 rand = new SecureRandom( );
139             }
140             RANDOM = rand;
141         }
142 
143         private static final String PROPERTY_PASSWORD_HASH_ITERATIONS = "password.hash.iterations";
144         private static final String PROPERTY_PASSWORD_HASH_LENGTH = "password.hash.length";
145 
146         /** number of iterations */
147         private final int _iterations;
148         /** salt */
149         private final byte [ ] _salt;
150         /** hashed password */
151         private final byte [ ] _hash;
152 
153         /**
154          * Construct a password from the stored representation
155          * 
156          * @param strStoredPassword
157          *            the stored representation
158          */
159         public PBKDF2Password( String strStoredPassword )
160         {
161             this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE );
162         }
163 
164         /**
165          * Construct a password
166          * 
167          * @param strPassword
168          *            the password text
169          * @param representation
170          *            representation of strPassword
171          */
172         public PBKDF2Password( String strPassword, PASSWORD_REPRESENTATION representation )
173         {
174             switch( representation )
175             {
176                 case CLEARTEXT:
177                     _iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, 40000 );
178                     int hashLength = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_LENGTH, 128 );
179                     try
180                     {
181                         _salt = new byte [ 16];
182                         RANDOM.nextBytes( _salt );
183                         PBEKeySpec spec = new PBEKeySpec( strPassword.toCharArray( ), _salt, _iterations, hashLength * 8 );
184                         SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
185                         _hash = skf.generateSecret( spec ).getEncoded( );
186                     }
187                     catch( NoSuchAlgorithmException | InvalidKeySpecException e )
188                     {
189                         throw new AppException( "Invalid Algo or key", e ); // should not happen
190                     }
191                     break;
192                 case STORABLE:
193                     Matcher matcher = FORMAT.matcher( strPassword );
194 
195                     if ( !matcher.matches( ) || matcher.groupCount( ) != 3 )
196                     {
197                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
198                     }
199                     _iterations = Integer.valueOf( matcher.group( 1 ) );
200                     try
201                     {
202                         _salt = Hex.decodeHex( matcher.group( 2 ).toCharArray( ) );
203                         _hash = Hex.decodeHex( matcher.group( 3 ).toCharArray( ) );
204                     }
205                     catch( DecoderException e )
206                     {
207                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
208                     }
209                     break;
210                 default:
211                     throw new IllegalArgumentException( representation.toString( ) );
212             }
213         }
214 
215         /**
216          * Get the PBKDF2 algorithm
217          * 
218          * @return the PBKDF2 algorithm to use
219          */
220         protected abstract String getAlgorithm( );
221 
222         @Override
223         public boolean check( String strCleartextPassword )
224         {
225             PBEKeySpec spec = new PBEKeySpec( strCleartextPassword.toCharArray( ), _salt, _iterations, _hash.length * 8 );
226             try
227             {
228                 SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
229                 byte [ ] testHash = skf.generateSecret( spec ).getEncoded( );
230                 return Arrays.equals( _hash, testHash );
231             }
232             catch( NoSuchAlgorithmException | InvalidKeySpecException e )
233             {
234                 throw new AppException( "Invalid Algo or key", e ); // should not happen
235             }
236         }
237 
238         /**
239          * Get the storage type identifier
240          * 
241          * @return the storage type identifier
242          */
243         protected abstract String getStorageType( );
244 
245         @Override
246         public String getStorableRepresentation( )
247         {
248             StringBuilder sb = new StringBuilder( );
249             sb.append( getStorageType( ) ).append( ':' );
250             sb.append( _iterations ).append( ':' ).append( Hex.encodeHex( _salt ) );
251             sb.append( ':' ).append( Hex.encodeHex( _hash ) );
252             return sb.toString( );
253         }
254 
255     }
256 
257     /**
258      * A Password stored using PBKDF2WithHmacSHA1
259      */
260     private static final class PBKDF2WithHmacSHA1Password extends PBKDF2Password
261     {
262 
263         /**
264          * Construct a password from the stored representation
265          * 
266          * @param strStoredPassword
267          *            the stored representation
268          */
269         public PBKDF2WithHmacSHA1Password( String strStoredPassword )
270         {
271             super( strStoredPassword );
272         }
273 
274         @Override
275         public boolean isLegacy( )
276         {
277             return true;
278         }
279 
280         @Override
281         protected String getAlgorithm( )
282         {
283             return "PBKDF2WithHmacSHA1";
284         }
285 
286         @Override
287         protected String getStorageType( )
288         {
289             return PBKDF2WITHHMACSHA1_STORAGE_TYPE;
290         }
291 
292         /**
293          * Legacy passwords must not be stored.
294          * 
295          * @return never returns
296          * @throws UnsupportedOperationException
297          *             Always thrown because Legacy passwords must not be stored.
298          */
299         @Override
300         public String getStorableRepresentation( )
301         {
302             throw new UnsupportedOperationException( "Must not store a legacy password" );
303         }
304 
305     }
306 
307     /**
308      * A Password stored using PBKDF2WithHmacSHA512
309      */
310     private static class PBKDF2WithHmacSHA512Password extends PBKDF2Password
311     {
312 
313         /**
314          * Construct a password from the stored representation
315          * 
316          * @param strStoredPassword
317          *            the stored representation
318          */
319         public PBKDF2WithHmacSHA512Password( String strStoredPassword )
320         {
321             super( strStoredPassword );
322         }
323 
324         /**
325          * Construct a password
326          * 
327          * @param strStoredPassword
328          *            the password text
329          * @param representation
330          *            representation of strPassword
331          */
332         public PBKDF2WithHmacSHA512Password( String strStoredPassword, PASSWORD_REPRESENTATION representation )
333         {
334             super( strStoredPassword, representation );
335         }
336 
337         @Override
338         public boolean isLegacy( )
339         {
340             return false;
341         }
342 
343         @Override
344         protected String getAlgorithm( )
345         {
346             return "PBKDF2WithHmacSHA512";
347         }
348 
349         @Override
350         protected String getStorageType( )
351         {
352             return PBKDF2WITHHMACSHA512_STORAGE_TYPE;
353         }
354 
355     }
356 
357     /**
358      * Dummy password which never matches a user password, but takes the same time as the PBKDF2Password to do so.
359      */
360     private static final class DummyPassword extends PBKDF2WithHmacSHA512Password
361     {
362         DummyPassword( )
363         {
364             // take the same time to construct as a proper PBKDF2Password
365             super( "", PASSWORD_REPRESENTATION.CLEARTEXT );
366         }
367 
368         @Override
369         public boolean check( String strCleartextPassword )
370         {
371             // take the same time to check as a proper PBKDF2Password
372             super.check( strCleartextPassword );
373             return false;
374         }
375 
376         @Override
377         public String getStorableRepresentation( )
378         {
379             throw new UnsupportedOperationException( "Must not store a dummy password" );
380         }
381     }
382 
383     /**
384      * Legacy password implementation super class
385      */
386     private abstract static class LegacyPassword implements IPassword
387     {
388         /**
389          * Legacy passwords are legacy
390          * 
391          * @return <code>true</code>
392          */
393         @Override
394         public final boolean isLegacy( )
395         {
396             return true;
397         }
398 
399         /**
400          * Legacy passwords must not be stored.
401          * 
402          * @return never returns
403          * @throws UnsupportedOperationException
404          *             Always thrown because Legacy passwords must not be stored.
405          */
406         @Override
407         public final String getStorableRepresentation( )
408         {
409             throw new UnsupportedOperationException( "Passwords should not be stored without proper hashing and salting" );
410         }
411 
412     }
413 
414     /**
415      * Password stored as plaintext
416      */
417     private static final class PlaintextPassword extends LegacyPassword
418     {
419 
420         /** the stored password */
421         private final String _strPassword;
422 
423         /**
424          * Constructor
425          * 
426          * @param strStoredPassword
427          *            the stored password
428          */
429         public PlaintextPassword( String strStoredPassword )
430         {
431             _strPassword = strStoredPassword;
432         }
433 
434         @Override
435         public boolean check( String strCleartextPassword )
436         {
437             return _strPassword != null && _strPassword.equals( strCleartextPassword );
438         }
439 
440     }
441 
442     /**
443      * Password stored as {@link MessageDigest} output
444      */
445     private static final class DigestPassword extends LegacyPassword
446     {
447         /** the stored password */
448         private final String _strPassword;
449         /** the digest algorithm */
450         private final String _strAlgorithm;
451 
452         /**
453          * Constructor
454          * 
455          * @param strAlgorithm
456          *            the digest algorithm
457          * @param strStoredPassword
458          *            the stored password
459          */
460         public DigestPassword( String strAlgorithm, String strStoredPassword )
461         {
462             _strPassword = strStoredPassword;
463             // check for algorithm support
464             try
465             {
466                 MessageDigest.getInstance( strAlgorithm );
467             }
468             catch( NoSuchAlgorithmException e )
469             {
470                 throw new IllegalArgumentException( e );
471             }
472             _strAlgorithm = strAlgorithm;
473         }
474 
475         @Override
476         public boolean check( String strCleartextPassword )
477         {
478             return _strPassword != null && _strPassword.equals( CryptoService.encrypt( strCleartextPassword, _strAlgorithm ) );
479         }
480     }
481 }