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         static final String PROPERTY_PASSWORD_HASH_ITERATIONS = "password.hash.iterations";
144         static final int DEFAULT_HASH_ITERATIONS = 210000;
145         private static final String PROPERTY_PASSWORD_HASH_LENGTH = "password.hash.length";
146 
147         /** number of iterations */
148         final int _iterations;
149         /** salt */
150         private final byte [ ] _salt;
151         /** hashed password */
152         private final byte [ ] _hash;
153 
154         /**
155          * Construct a password from the stored representation
156          * 
157          * @param strStoredPassword
158          *            the stored representation
159          */
160         public PBKDF2Password( String strStoredPassword )
161         {
162             this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE );
163         }
164 
165         /**
166          * Construct a password
167          * 
168          * @param strPassword
169          *            the password text
170          * @param representation
171          *            representation of strPassword
172          */
173         public PBKDF2Password( String strPassword, PASSWORD_REPRESENTATION representation )
174         {
175             switch( representation )
176             {
177                 case CLEARTEXT:
178                     _iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, DEFAULT_HASH_ITERATIONS );
179                     int hashLength = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_LENGTH, 128 );
180                     try
181                     {
182                         _salt = new byte [ 16];
183                         RANDOM.nextBytes( _salt );
184                         PBEKeySpec spec = new PBEKeySpec( strPassword.toCharArray( ), _salt, _iterations, hashLength * 8 );
185                         SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
186                         _hash = skf.generateSecret( spec ).getEncoded( );
187                     }
188                     catch( NoSuchAlgorithmException | InvalidKeySpecException e )
189                     {
190                         throw new AppException( "Invalid Algo or key", e ); // should not happen
191                     }
192                     break;
193                 case STORABLE:
194                     Matcher matcher = FORMAT.matcher( strPassword );
195 
196                     if ( !matcher.matches( ) || matcher.groupCount( ) != 3 )
197                     {
198                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
199                     }
200                     _iterations = Integer.valueOf( matcher.group( 1 ) );
201                     try
202                     {
203                         _salt = Hex.decodeHex( matcher.group( 2 ).toCharArray( ) );
204                         _hash = Hex.decodeHex( matcher.group( 3 ).toCharArray( ) );
205                     }
206                     catch( DecoderException e )
207                     {
208                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
209                     }
210                     break;
211                 default:
212                     throw new IllegalArgumentException( representation.toString( ) );
213             }
214         }
215 
216         /**
217          * Get the PBKDF2 algorithm
218          * 
219          * @return the PBKDF2 algorithm to use
220          */
221         protected abstract String getAlgorithm( );
222 
223         @Override
224         public boolean check( String strCleartextPassword )
225         {
226             PBEKeySpec spec = new PBEKeySpec( strCleartextPassword.toCharArray( ), _salt, _iterations, _hash.length * 8 );
227             try
228             {
229                 SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
230                 byte [ ] testHash = skf.generateSecret( spec ).getEncoded( );
231                 return Arrays.equals( _hash, testHash );
232             }
233             catch( NoSuchAlgorithmException | InvalidKeySpecException e )
234             {
235                 throw new AppException( "Invalid Algo or key", e ); // should not happen
236             }
237         }
238 
239         /**
240          * Get the storage type identifier
241          * 
242          * @return the storage type identifier
243          */
244         protected abstract String getStorageType( );
245 
246         @Override
247         public String getStorableRepresentation( )
248         {
249             StringBuilder sb = new StringBuilder( );
250             sb.append( getStorageType( ) ).append( ':' );
251             sb.append( _iterations ).append( ':' ).append( Hex.encodeHex( _salt ) );
252             sb.append( ':' ).append( Hex.encodeHex( _hash ) );
253             return sb.toString( );
254         }
255 
256     }
257 
258     /**
259      * A Password stored using PBKDF2WithHmacSHA1
260      */
261     private static final class PBKDF2WithHmacSHA1Password extends PBKDF2Password
262     {
263 
264         /**
265          * Construct a password from the stored representation
266          * 
267          * @param strStoredPassword
268          *            the stored representation
269          */
270         public PBKDF2WithHmacSHA1Password( String strStoredPassword )
271         {
272             super( strStoredPassword );
273         }
274 
275         @Override
276         public boolean isLegacy( )
277         {
278             return true;
279         }
280 
281         @Override
282         protected String getAlgorithm( )
283         {
284             return "PBKDF2WithHmacSHA1";
285         }
286 
287         @Override
288         protected String getStorageType( )
289         {
290             return PBKDF2WITHHMACSHA1_STORAGE_TYPE;
291         }
292 
293         /**
294          * Legacy passwords must not be stored.
295          * 
296          * @return never returns
297          * @throws UnsupportedOperationException
298          *             Always thrown because Legacy passwords must not be stored.
299          */
300         @Override
301         public String getStorableRepresentation( )
302         {
303             throw new UnsupportedOperationException( "Must not store a legacy password" );
304         }
305 
306     }
307 
308     /**
309      * A Password stored using PBKDF2WithHmacSHA512
310      */
311     private static class PBKDF2WithHmacSHA512Password extends PBKDF2Password
312     {
313 
314         /**
315          * Construct a password from the stored representation
316          * 
317          * @param strStoredPassword
318          *            the stored representation
319          */
320         public PBKDF2WithHmacSHA512Password( String strStoredPassword )
321         {
322             super( strStoredPassword );
323         }
324 
325         /**
326          * Construct a password
327          * 
328          * @param strStoredPassword
329          *            the password text
330          * @param representation
331          *            representation of strPassword
332          */
333         public PBKDF2WithHmacSHA512Password( String strStoredPassword, PASSWORD_REPRESENTATION representation )
334         {
335             super( strStoredPassword, representation );
336         }
337 
338         @Override
339         public boolean isLegacy( )
340         {
341             int iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, DEFAULT_HASH_ITERATIONS );
342             return _iterations < iterations;
343         }
344 
345         @Override
346         protected String getAlgorithm( )
347         {
348             return "PBKDF2WithHmacSHA512";
349         }
350 
351         @Override
352         protected String getStorageType( )
353         {
354             return PBKDF2WITHHMACSHA512_STORAGE_TYPE;
355         }
356 
357     }
358 
359     /**
360      * Dummy password which never matches a user password, but takes the same time as the PBKDF2Password to do so.
361      */
362     private static final class DummyPassword extends PBKDF2WithHmacSHA512Password
363     {
364         DummyPassword( )
365         {
366             // take the same time to construct as a proper PBKDF2Password
367             super( "", PASSWORD_REPRESENTATION.CLEARTEXT );
368         }
369 
370         @Override
371         public boolean check( String strCleartextPassword )
372         {
373             // take the same time to check as a proper PBKDF2Password
374             super.check( strCleartextPassword );
375             return false;
376         }
377 
378         @Override
379         public String getStorableRepresentation( )
380         {
381             throw new UnsupportedOperationException( "Must not store a dummy password" );
382         }
383     }
384 
385     /**
386      * Legacy password implementation super class
387      */
388     private abstract static class LegacyPassword implements IPassword
389     {
390         /**
391          * Legacy passwords are legacy
392          * 
393          * @return <code>true</code>
394          */
395         @Override
396         public final boolean isLegacy( )
397         {
398             return true;
399         }
400 
401         /**
402          * Legacy passwords must not be stored.
403          * 
404          * @return never returns
405          * @throws UnsupportedOperationException
406          *             Always thrown because Legacy passwords must not be stored.
407          */
408         @Override
409         public final String getStorableRepresentation( )
410         {
411             throw new UnsupportedOperationException( "Passwords should not be stored without proper hashing and salting" );
412         }
413 
414     }
415 
416     /**
417      * Password stored as plaintext
418      */
419     private static final class PlaintextPassword extends LegacyPassword
420     {
421 
422         /** the stored password */
423         private final String _strPassword;
424 
425         /**
426          * Constructor
427          * 
428          * @param strStoredPassword
429          *            the stored password
430          */
431         public PlaintextPassword( String strStoredPassword )
432         {
433             _strPassword = strStoredPassword;
434         }
435 
436         @Override
437         public boolean check( String strCleartextPassword )
438         {
439             return _strPassword != null && _strPassword.equals( strCleartextPassword );
440         }
441 
442     }
443 
444     /**
445      * Password stored as {@link MessageDigest} output
446      */
447     private static final class DigestPassword extends LegacyPassword
448     {
449         /** the stored password */
450         private final String _strPassword;
451         /** the digest algorithm */
452         private final String _strAlgorithm;
453 
454         /**
455          * Constructor
456          * 
457          * @param strAlgorithm
458          *            the digest algorithm
459          * @param strStoredPassword
460          *            the stored password
461          */
462         public DigestPassword( String strAlgorithm, String strStoredPassword )
463         {
464             _strPassword = strStoredPassword;
465             // check for algorithm support
466             try
467             {
468                 MessageDigest.getInstance( strAlgorithm );
469             }
470             catch( NoSuchAlgorithmException e )
471             {
472                 throw new IllegalArgumentException( e );
473             }
474             _strAlgorithm = strAlgorithm;
475         }
476 
477         @Override
478         public boolean check( String strCleartextPassword )
479         {
480             return _strPassword != null && _strPassword.equals( CryptoService.encrypt( strCleartextPassword, _strAlgorithm ) );
481         }
482     }
483 }