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.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
60
61 final class PasswordFactory implements IPasswordFactory
62 {
63
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
110
111 private abstract static class PBKDF2Password implements IPassword
112 {
113
114
115
116
117 enum PASSWORD_REPRESENTATION
118 {
119 CLEARTEXT,
120 STORABLE
121 }
122
123
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
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
147 private final int _iterations;
148
149 private final byte [ ] _salt;
150
151 private final byte [ ] _hash;
152
153
154
155
156
157
158
159 public PBKDF2Password( String strStoredPassword )
160 {
161 this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE );
162 }
163
164
165
166
167
168
169
170
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 );
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
217
218
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 );
235 }
236 }
237
238
239
240
241
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
259
260 private static final class PBKDF2WithHmacSHA1Password extends PBKDF2Password
261 {
262
263
264
265
266
267
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
294
295
296
297
298
299 @Override
300 public String getStorableRepresentation( )
301 {
302 throw new UnsupportedOperationException( "Must not store a legacy password" );
303 }
304
305 }
306
307
308
309
310 private static class PBKDF2WithHmacSHA512Password extends PBKDF2Password
311 {
312
313
314
315
316
317
318
319 public PBKDF2WithHmacSHA512Password( String strStoredPassword )
320 {
321 super( strStoredPassword );
322 }
323
324
325
326
327
328
329
330
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
359
360 private static final class DummyPassword extends PBKDF2WithHmacSHA512Password
361 {
362 DummyPassword( )
363 {
364
365 super( "", PASSWORD_REPRESENTATION.CLEARTEXT );
366 }
367
368 @Override
369 public boolean check( String strCleartextPassword )
370 {
371
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
385
386 private abstract static class LegacyPassword implements IPassword
387 {
388
389
390
391
392
393 @Override
394 public final boolean isLegacy( )
395 {
396 return true;
397 }
398
399
400
401
402
403
404
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
416
417 private static final class PlaintextPassword extends LegacyPassword
418 {
419
420
421 private final String _strPassword;
422
423
424
425
426
427
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
444
445 private static final class DigestPassword extends LegacyPassword
446 {
447
448 private final String _strPassword;
449
450 private final String _strAlgorithm;
451
452
453
454
455
456
457
458
459
460 public DigestPassword( String strAlgorithm, String strStoredPassword )
461 {
462 _strPassword = strStoredPassword;
463
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 }