1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
59
60 final class PasswordFactory implements IPasswordFactory
61 {
62
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
105
106 private static class PBKDF2Password implements IPassword
107 {
108
109
110
111
112
113 static enum PASSWORD_REPRESENTATION
114 {
115 CLEARTEXT,
116 STORABLE
117 }
118
119
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
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
142 private final int _iterations;
143
144 private final byte[] _salt;
145
146 private final byte[] _hash;
147
148
149
150
151
152 public PBKDF2Password( String strStoredPassword )
153 {
154 this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE);
155 }
156
157
158
159
160
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 );
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 );
215 }
216 }
217
218
219
220
221
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
243
244
245 private static final class DummyPassword extends PBKDF2Password
246 {
247 DummyPassword( )
248 {
249
250 super("", PASSWORD_REPRESENTATION.CLEARTEXT);
251 }
252
253 @Override
254 public boolean check( String strCleartextPassword )
255 {
256
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
270
271 private static abstract class LegacyPassword implements IPassword
272 {
273
274
275
276
277 @Override
278 public final boolean isLegacy( )
279 {
280 return true;
281 }
282
283
284
285
286
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
298
299 private static final class PlaintextPassword extends LegacyPassword
300 {
301
302
303 private final String _strPassword;
304
305
306
307
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
324
325 private final static class DigestPassword extends LegacyPassword
326 {
327
328 private final String _strPassword;
329
330 private final String _strAlgorithm;
331
332
333
334
335
336
337 public DigestPassword( String strAlgorithm, String strStoredPassword )
338 {
339 _strPassword = strStoredPassword;
340
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 }