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