View Javadoc
1   /*
2    * Copyright (c) 2015, Mairie de 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.AppLogService;
52  import fr.paris.lutece.portal.service.util.AppPropertiesService;
53  import fr.paris.lutece.portal.service.util.CryptoService;
54  import fr.paris.lutece.util.password.IPassword;
55  import fr.paris.lutece.util.password.IPasswordFactory;
56  
57  /**
58   * A factory for getting storable password representation
59   */
60  final class PasswordFactory implements IPasswordFactory
61  {
62      // storage types
63      private static final String PBKDF2_STORAGE_TYPE = "PBKDF2";
64      private static final String PLAINTEXT_STORAGE_TYPE = "PLAINTEXT";
65      private static final String DUMMY_STORAGE_TYPE = "\0DUMMY\0";
66      private static final String DUMMY_STORED_PASSWORD = DUMMY_STORAGE_TYPE + ":\0";
67  
68      @Override
69      public IPassword getPassword( String strStoredPassword )
70      {
71          int storageTypeSeparatorIndex = strStoredPassword.indexOf( ':' );
72          if ( storageTypeSeparatorIndex == -1 )
73          {
74              throw new IllegalArgumentException( strStoredPassword );
75          }
76          String storageType = strStoredPassword.substring( 0, storageTypeSeparatorIndex );
77          String password = strStoredPassword.substring( storageTypeSeparatorIndex  + 1 );
78          switch ( storageType )
79          {
80          case PLAINTEXT_STORAGE_TYPE:
81              return new PlaintextPassword( password );
82          case PBKDF2_STORAGE_TYPE:
83              return new PBKDF2Password( password );
84          case DUMMY_STORAGE_TYPE:
85              return new DummyPassword(  );
86          default:
87              return new DigestPassword( storageType, password );
88          }
89      }
90  
91      @Override
92      public IPassword getPasswordFromCleartext( String strUserPassword )
93      {
94          return new PBKDF2Password( strUserPassword, PBKDF2Password.PASSWORD_REPRESENTATION.CLEARTEXT );
95      }
96  
97      @Override
98      public IPassword getDummyPassword( )
99      {
100         return getPassword( DUMMY_STORED_PASSWORD );
101     }
102 
103     /**
104      * A Password stored using PBKDF2WithHmacSHA1
105      */
106     private static class PBKDF2Password implements IPassword
107     {
108 
109         /**
110          * Enum to specify if the password is constructed from
111          * cleartext or hashed form
112          */
113         static enum PASSWORD_REPRESENTATION
114         {
115             CLEARTEXT,
116             STORABLE
117         }
118 
119         /** Storage format : iterations:hex(salt):hex(hash) */
120         private static final Pattern FORMAT = Pattern.compile( "^(\\d+):([a-z0-9]+):([a-z0-9]+)$", Pattern.CASE_INSENSITIVE );
121         private static final Random RANDOM;
122 
123         // init the random number generator
124         static
125         {
126             Random rand;
127             try
128             {
129                 rand = SecureRandom.getInstance("SHA1PRNG");
130             } catch ( NoSuchAlgorithmException e )
131             {
132                 AppLogService.error( "SHA1PRNG is not availabled. Picking the default SecureRandom.", e );
133                 rand = new SecureRandom( );
134             }
135             RANDOM = rand;
136         }
137 
138         private static final String PROPERTY_PASSWORD_HASH_ITERATIONS = "password.hash.iterations";
139         private static final String PROPERTY_PASSWORD_HASH_LENGTH = "password.hash.length";
140 
141         /** number of iterations */
142         private final int _iterations;
143         /** salt */
144         private final byte[] _salt;
145         /** hashed password */
146         private final byte[] _hash;
147 
148         /**
149          * Construct a password from the stored representation
150          * @param strStoredPassword the stored representation
151          */
152         public PBKDF2Password( String strStoredPassword )
153         {
154             this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE);
155         }
156 
157         /**
158          * Construct a password
159          * @param strPassword the password text
160          * @param representation representation of strPassword
161          */
162         public PBKDF2Password( String strPassword, PASSWORD_REPRESENTATION representation )
163         {
164             switch ( representation )
165             {
166             case CLEARTEXT:
167                 _iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, 40000 );
168                 int hashLength = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_LENGTH, 128 );
169                 try
170                 {
171                     _salt = new byte[16];
172                     RANDOM.nextBytes(_salt);
173                     PBEKeySpec spec = new PBEKeySpec(strPassword.toCharArray( ), _salt, _iterations, hashLength * 8);
174                     SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
175                     _hash = skf.generateSecret(spec).getEncoded();
176                 } catch ( NoSuchAlgorithmException | InvalidKeySpecException e )
177                 {
178                     throw new RuntimeException( e ); // should not happen
179                 }
180                 break;
181             case STORABLE:
182                 Matcher matcher = FORMAT.matcher( strPassword );
183 
184                 if ( !matcher.matches( ) || matcher.groupCount( ) != 3)
185                 {
186                     throw new IllegalArgumentException( "Invalid stored password " + strPassword );
187                 }
188                 _iterations = Integer.valueOf( matcher.group( 1 ) );
189                 try
190                 {
191                     _salt = Hex.decodeHex( matcher.group( 2 ).toCharArray( ) );
192                     _hash = Hex.decodeHex( matcher.group( 3 ).toCharArray( ) );
193                 } catch ( DecoderException e )
194                 {
195                     throw new IllegalArgumentException( "Invalid stored password " + strPassword );
196                 }
197                 break;
198             default:
199                 throw new IllegalArgumentException( representation.toString( ) );
200             }
201         }
202 
203         @Override
204         public boolean check( String strCleartextPassword )
205         {
206             PBEKeySpec spec = new PBEKeySpec( strCleartextPassword.toCharArray( ), _salt, _iterations, _hash.length * 8);
207             try
208             {
209                 SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
210                 byte[] testHash = skf.generateSecret( spec ).getEncoded( );
211                 return Arrays.equals( _hash, testHash );
212             } catch ( NoSuchAlgorithmException | InvalidKeySpecException e )
213             {
214                 throw new RuntimeException( e ); // should not happen
215             }
216         }
217 
218         /**
219          * {@inheritDoc}
220          *
221          * Only implementation which is not legacy.
222          */
223         @Override
224         public final boolean isLegacy( )
225         {
226             return false;
227         }
228 
229         @Override
230         public String getStorableRepresentation( )
231         {
232             StringBuilder sb = new StringBuilder( );
233             sb.append( PBKDF2_STORAGE_TYPE ).append( ':' );
234             sb.append( _iterations ).append( ':' ).append( Hex.encodeHex( _salt ) );
235             sb.append( ':' ).append( Hex.encodeHex( _hash ) );
236             return sb.toString( );
237         }
238 
239     }
240 
241     /**
242      * Dummy password which never matches a user password, but takes
243      * the same time as the PBKDF2Password to do so.
244      */
245     private static final class DummyPassword extends PBKDF2Password
246     {
247         DummyPassword(  )
248         {
249             // take the same time to construct as a proper PBKDF2Password
250             super("", PASSWORD_REPRESENTATION.CLEARTEXT);
251         }
252 
253         @Override
254         public boolean check( String strCleartextPassword )
255         {
256             // take the same time to check as a proper PBKDF2Password
257             super.check( strCleartextPassword );
258             return false;
259         }
260 
261         @Override
262         public String getStorableRepresentation( )
263         {
264             throw new UnsupportedOperationException( "Must not store a dummy password" );
265         }
266     }
267 
268     /**
269      * Legacy password implementation super class
270      */
271     private static abstract class LegacyPassword implements IPassword
272     {
273         /**
274          * Legacy passwords are legacy
275          * @return <code>true</code>
276          */
277         @Override
278         public final boolean isLegacy( )
279         {
280             return true;
281         }
282 
283         /**
284          * Legacy passwords must not be stored.
285          * @return never returns
286          * @throws UnsupportedOperationException
287          */
288         @Override
289         public final String getStorableRepresentation( )
290         {
291             throw new UnsupportedOperationException( "Passwords should not be stored without proper hashing and salting" );
292         }
293 
294     }
295 
296     /**
297      * Password stored as plaintext
298      */
299     private static final class PlaintextPassword extends LegacyPassword
300     {
301 
302         /** the stored password */
303         private final String _strPassword;
304 
305         /**
306          * Constructor
307          * @param strStoredPassword the stored password
308          */
309         public PlaintextPassword( String strStoredPassword )
310         {
311             _strPassword = strStoredPassword;
312         }
313 
314         @Override
315         public boolean check( String strCleartextPassword )
316         {
317             return _strPassword != null && _strPassword.equals( strCleartextPassword );
318         }
319 
320     }
321 
322     /**
323      * Password stored as {@link MessageDigest} output
324      */
325     private final static class DigestPassword extends LegacyPassword
326     {
327         /** the stored password */
328         private final String _strPassword;
329         /** the digest algorithm */
330         private final String _strAlgorithm;
331 
332         /**
333          * Constructor
334          * @param strAlgorithm the digest algorithm
335          * @param strStoredPassword the stored password
336          */
337         public DigestPassword( String strAlgorithm, String strStoredPassword )
338         {
339             _strPassword = strStoredPassword;
340             // check for algorithm support
341             try
342             {
343                 MessageDigest.getInstance( strAlgorithm );
344             } catch ( NoSuchAlgorithmException e )
345             {
346                 throw new IllegalArgumentException( e );
347             }
348             _strAlgorithm = strAlgorithm;
349         }
350 
351         @Override
352         public boolean check( String strCleartextPassword )
353         {
354             return _strPassword != null && _strPassword.equals( CryptoService.encrypt( strCleartextPassword, _strAlgorithm ) );
355         }
356     }
357 }