1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 *
19 */
20 package org.apache.directory.server.core.authn;
21
22
23 import java.io.UnsupportedEncodingException;
24 import java.security.MessageDigest;
25 import java.security.NoSuchAlgorithmException;
26 import java.security.SecureRandom;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.Set;
32
33 import javax.naming.Context;
34 import javax.naming.NamingException;
35
36 import org.apache.commons.collections.map.LRUMap;
37 import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
38 import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
39 import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
40 import org.apache.directory.server.core.entry.ServerEntry;
41 import org.apache.directory.server.core.entry.ServerStringValue;
42 import org.apache.directory.server.core.event.EventInterceptor;
43 import org.apache.directory.server.core.exception.ExceptionInterceptor;
44 import org.apache.directory.server.core.interceptor.context.BindOperationContext;
45 import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
46 import org.apache.directory.server.core.normalization.NormalizationInterceptor;
47 import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
48 import org.apache.directory.server.core.schema.SchemaInterceptor;
49 import org.apache.directory.server.core.subtree.SubentryInterceptor;
50 import org.apache.directory.server.core.trigger.TriggerInterceptor;
51 import org.apache.directory.shared.ldap.constants.AuthenticationLevel;
52 import org.apache.directory.shared.ldap.constants.LdapSecurityConstants;
53 import org.apache.directory.shared.ldap.constants.SchemaConstants;
54 import org.apache.directory.shared.ldap.entry.EntryAttribute;
55 import org.apache.directory.shared.ldap.entry.Value;
56 import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
57 import org.apache.directory.shared.ldap.name.LdapDN;
58 import org.apache.directory.shared.ldap.util.ArrayUtils;
59 import org.apache.directory.shared.ldap.util.Base64;
60 import org.apache.directory.shared.ldap.util.StringTools;
61 import org.apache.directory.shared.ldap.util.UnixCrypt;
62
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66
67 /**
68 * A simple {@link Authenticator} that authenticates clear text passwords
69 * contained within the <code>userPassword</code> attribute in DIT. If the
70 * password is stored with a one-way encryption applied (e.g. SHA), the password
71 * is hashed the same way before comparison.
72 *
73 * We use a cache to speedup authentication, where the DN/password are stored.
74 *
75 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
76 */
77 public class SimpleAuthenticator extends AbstractAuthenticator
78 {
79 private static final Logger LOG = LoggerFactory.getLogger( SimpleAuthenticator.class );
80
81 /** A speedup for logger in debug mode */
82 private static final boolean IS_DEBUG = LOG.isDebugEnabled();
83
84 /**
85 * A cache to store passwords. It's a speedup, we will be able to avoid backend lookups.
86 *
87 * Note that the backend also use a cache mechanism, but for performance gain, it's good
88 * to manage a cache here. The main problem is that when a user modify his password, we will
89 * have to update it at three different places :
90 * - in the backend,
91 * - in the partition cache,
92 * - in this cache.
93 *
94 * The update of the backend and partition cache is already correctly handled, so we will
95 * just have to offer an access to refresh the local cache.
96 *
97 * We need to be sure that frequently used passwords be always in cache, and not discarded.
98 * We will use a LRU cache for this purpose.
99 */
100 private final LRUMap credentialCache;
101
102 /** Declare a default for this cache. 100 entries seems to be enough */
103 private static final int DEFAULT_CACHE_SIZE = 100;
104
105 /**
106 * Define the interceptors we should *not* go through when we will have to request the backend
107 * about a userPassword.
108 */
109 private static final Collection<String> USERLOOKUP_BYPASS;
110
111
112 static
113 {
114 Set<String> c = new HashSet<String>();
115 c.add( NormalizationInterceptor.class.getName() );
116 c.add( AuthenticationInterceptor.class.getName() );
117 c.add( AciAuthorizationInterceptor.class.getName() );
118 c.add( DefaultAuthorizationInterceptor.class.getName() );
119 c.add( ExceptionInterceptor.class.getName() );
120 c.add( OperationalAttributeInterceptor.class.getName() );
121 c.add( SchemaInterceptor.class.getName() );
122 c.add( SubentryInterceptor.class.getName() );
123 c.add( CollectiveAttributeInterceptor.class.getName() );
124 c.add( EventInterceptor.class.getName() );
125 c.add( TriggerInterceptor.class.getName() );
126 USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c );
127 }
128
129
130 /**
131 * Creates a new instance.
132 * @see AbstractAuthenticator
133 */
134 public SimpleAuthenticator()
135 {
136 super( AuthenticationLevel.SIMPLE.toString() );
137 credentialCache = new LRUMap( DEFAULT_CACHE_SIZE );
138 }
139
140
141 /**
142 * Creates a new instance, with an initial cache size
143 * @param cacheSize the size of the credential cache
144 */
145 public SimpleAuthenticator( int cacheSize )
146 {
147 super( AuthenticationLevel.SIMPLE.toString() );
148
149 credentialCache = new LRUMap( cacheSize > 0 ? cacheSize : DEFAULT_CACHE_SIZE );
150 }
151
152
153 /**
154 * A private class to store all informations about the existing
155 * password found in the cache or get from the backend.
156 *
157 * This is necessary as we have to compute :
158 * - the used algorithm
159 * - the salt if any
160 * - the password itself.
161 *
162 * If we have a on-way encrypted password, it is stored using this
163 * format :
164 * {<algorithm>}<encrypted password>
165 * where the encrypted password format can be :
166 * - MD5/SHA : base64([<salt (8 bytes)>]<password>)
167 * - crypt : <salt (2 btytes)><password>
168 *
169 * Algorithm are currently MD5, SMD5, SHA, SSHA, CRYPT and empty
170 */
171 private class EncryptionMethod
172 {
173 private byte[] salt;
174 private LdapSecurityConstants algorithm;
175
176 private EncryptionMethod( LdapSecurityConstants algorithm, byte[] salt )
177 {
178 this.algorithm = algorithm;
179 this.salt = salt;
180 }
181 }
182
183
184 /**
185 * Get the password either from cache or from backend.
186 * @param principalDN The DN from which we want the password
187 * @return A byte array which can be empty if the password was not found
188 * @throws NamingException If we have a problem during the lookup operation
189 */
190 private LdapPrincipal getStoredPassword( BindOperationContext opContext ) throws Exception
191 {
192 LdapPrincipal principal = null;
193
194 synchronized( credentialCache )
195 {
196 principal = ( LdapPrincipal ) credentialCache.get( opContext.getDn().getNormName() );
197 }
198
199 byte[] storedPassword;
200
201 if ( principal == null )
202 {
203 // Not found in the cache
204 // Get the user password from the backend
205 storedPassword = lookupUserPassword( opContext );
206
207
208 // Deal with the special case where the user didn't enter a password
209 // We will compare the empty array with the credentials. Sometime,
210 // a user does not set a password. This is bad, but there is nothing
211 // we can do against that, except education ...
212 if ( storedPassword == null )
213 {
214 storedPassword = ArrayUtils.EMPTY_BYTE_ARRAY;
215 }
216
217 // Create the new principal before storing it in the cache
218 principal = new LdapPrincipal( opContext.getDn(), AuthenticationLevel.SIMPLE, storedPassword );
219
220 // Now, update the local cache.
221 synchronized( credentialCache )
222 {
223 credentialCache.put( opContext.getDn().getNormName(), principal );
224 }
225 }
226
227 return principal;
228 }
229
230
231 /**
232 * Looks up <tt>userPassword</tt> attribute of the entry whose name is the
233 * value of {@link Context#SECURITY_PRINCIPAL} environment variable, and
234 * authenticates a user with the plain-text password.
235 *
236 * We have at least 6 algorithms to encrypt the password :
237 * - SHA
238 * - SSHA (salted SHA)
239 * - MD5
240 * - SMD5 (slated MD5)
241 * - crypt (unix crypt)
242 * - plain text, ie no encryption.
243 *
244 * If we get an encrypted password, it is prefixed by the used algorithm, between
245 * brackets : {SSHA}password ...
246 *
247 * If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password :
248 * - length(password) - 20, starting at 21th position for SSHA
249 * - length(password) - 16, starting at 16th position for SMD5
250 * - length(password) - 2, starting at 3rd position for crypt
251 *
252 * For (S)SHA and (S)MD5, we have to transform the password from Base64 encoded text
253 * to a byte[] before comparing the password with the stored one.
254 * For crypt, we only have to remove the salt.
255 *
256 * At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for
257 * the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords.
258 *
259 * The stored password is always using the unsalted form, and is stored as a bytes array.
260 */
261 public LdapPrincipal authenticate( BindOperationContext opContext ) throws Exception
262 {
263 if ( IS_DEBUG )
264 {
265 LOG.debug( "Authenticating {}", opContext.getDn() );
266 }
267
268 // ---- extract password from JNDI environment
269 byte[] credentials = opContext.getCredentials();
270
271 LdapPrincipal principal = getStoredPassword( opContext );
272
273 // Get the stored password, either from cache or from backend
274 byte[] storedPassword = principal.getUserPassword();
275
276 // Short circuit for PLAIN TEXT passwords : we compare the byte array directly
277 // Are the passwords equal ?
278 if ( Arrays.equals( credentials, storedPassword ) )
279 {
280 if ( IS_DEBUG )
281 {
282 LOG.debug( "{} Authenticated", opContext.getDn() );
283 }
284
285 return principal;
286 }
287
288 // Let's see if the stored password was encrypted
289 LdapSecurityConstants algorithm = findAlgorithm( storedPassword );
290
291 if ( algorithm != null )
292 {
293 EncryptionMethod encryptionMethod = new EncryptionMethod( algorithm, null );
294
295 // Let's get the encrypted part of the stored password
296 // We should just keep the password, excluding the algorithm
297 // and the salt, if any.
298 // But we should also get the algorithm and salt to
299 // be able to encrypt the submitted user password in the next step
300 byte[] encryptedStored = splitCredentials( storedPassword, encryptionMethod );
301
302 // Reuse the slatedPassword informations to construct the encrypted
303 // password given by the user.
304 byte[] userPassword = encryptPassword( credentials, encryptionMethod );
305
306 // Now, compare the two passwords.
307 if ( Arrays.equals( userPassword, encryptedStored ) )
308 {
309 if ( IS_DEBUG )
310 {
311 LOG.debug( "{} Authenticated", opContext.getDn() );
312 }
313
314 return principal;
315 }
316 else
317 {
318 // Bad password ...
319 String message = "Password not correct for user '" + opContext.getDn().getUpName() + "'";
320 LOG.info( message );
321 throw new LdapAuthenticationException(message);
322 }
323 }
324 else
325 {
326 // Bad password ...
327 String message = "Password not correct for user '" + opContext.getDn().getUpName() + "'";
328 LOG.info( message );
329 throw new LdapAuthenticationException(message);
330 }
331 }
332
333
334 private static void split( byte[] all, int offset, byte[] left, byte[] right )
335 {
336 System.arraycopy( all, offset, left, 0, left.length );
337 System.arraycopy( all, offset + left.length, right, 0, right.length );
338 }
339
340
341 /**
342 * Decopose the stored password in an algorithm, an eventual salt
343 * and the password itself.
344 *
345 * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
346 * is base64 encoded
347 *
348 * @param encryptionMethod The structure to feed
349 * @return The password
350 * @param credentials the credentials to split
351 */
352 private byte[] splitCredentials( byte[] credentials, EncryptionMethod encryptionMethod )
353 {
354 int pos = encryptionMethod.algorithm.getName().length() + 2;
355
356 switch ( encryptionMethod.algorithm )
357 {
358 case HASH_METHOD_MD5 :
359 case HASH_METHOD_SHA :
360 try
361 {
362 // We just have the password just after the algorithm, base64 encoded.
363 // Just decode the password and return it.
364 return Base64.decode( new String( credentials, pos, credentials.length - pos, "UTF-8" ).toCharArray() );
365 }
366 catch ( UnsupportedEncodingException uee )
367 {
368 // do nothing
369 return credentials;
370 }
371
372 case HASH_METHOD_SMD5 :
373 case HASH_METHOD_SSHA :
374 try
375 {
376 // The password is associated with a salt. Decompose it
377 // in two parts, after having decoded the password.
378 // The salt will be stored into the EncryptionMethod structure
379 // The salt is at the end of the credentials, and is 8 bytes long
380 byte[] passwordAndSalt = Base64.decode( new String( credentials, pos, credentials.length - pos, "UTF-8" ).
381 toCharArray() );
382
383 encryptionMethod.salt = new byte[8];
384 byte[] password = new byte[passwordAndSalt.length - encryptionMethod.salt.length];
385 split( passwordAndSalt, 0, password, encryptionMethod.salt );
386
387 return password;
388 }
389 catch ( UnsupportedEncodingException uee )
390 {
391 // do nothing
392 return credentials;
393 }
394
395 case HASH_METHOD_CRYPT :
396 // The password is associated with a salt. Decompose it
397 // in two parts, storing the salt into the EncryptionMethod structure.
398 // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
399 encryptionMethod.salt = new byte[2];
400 byte[] password = new byte[credentials.length - encryptionMethod.salt.length - pos];
401 split( credentials, pos, encryptionMethod.salt, password );
402
403 return password;
404
405 default :
406 // unknown method
407 return credentials;
408
409 }
410 }
411
412
413 /**
414 * Get the algorithm from the stored password.
415 * It can be found on the beginning of the stored password, between
416 * curly brackets.
417 * @param credentials the credentials of the user
418 * @return the name of the algorithm to use
419 * TODO use an enum for the algorithm
420 */
421 private LdapSecurityConstants findAlgorithm( byte[] credentials )
422 {
423 if ( ( credentials == null ) || ( credentials.length == 0 ) )
424 {
425 return null;
426 }
427
428 if ( credentials[0] == '{' )
429 {
430 // get the algorithm
431 int pos = 1;
432
433 while ( pos < credentials.length )
434 {
435 if ( credentials[pos] == '}' )
436 {
437 break;
438 }
439
440 pos++;
441 }
442
443 if ( pos < credentials.length )
444 {
445 if ( pos == 1 )
446 {
447 // We don't have an algorithm : return the credentials as is
448 return null;
449 }
450
451 String algorithm = new String( credentials, 1, pos - 1 ).toLowerCase();
452
453 return LdapSecurityConstants.getAlgorithm( algorithm );
454 }
455 else
456 {
457 // We don't have an algorithm
458 return null;
459 }
460 }
461 else
462 {
463 // No '{algo}' part
464 return null;
465 }
466 }
467
468
469 /**
470 * Compute the hashed password given an algorithm, the credentials and
471 * an optional salt.
472 *
473 * @param algorithm the algorithm to use
474 * @param password the credentials
475 * @param salt the optional salt
476 * @return the digested credentials
477 */
478 private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
479 {
480 MessageDigest digest;
481
482 try
483 {
484 digest = MessageDigest.getInstance( algorithm.getName() );
485 }
486 catch ( NoSuchAlgorithmException e1 )
487 {
488 return null;
489 }
490
491 if ( salt != null )
492 {
493 digest.update( password );
494 digest.update( salt );
495 return digest.digest();
496 }
497 else
498 {
499 return digest.digest( password );
500 }
501 }
502
503
504 private byte[] encryptPassword( byte[] credentials, EncryptionMethod encryptionMethod )
505 {
506 byte[] salt = encryptionMethod.salt;
507
508 switch ( encryptionMethod.algorithm )
509 {
510 case HASH_METHOD_SHA :
511 case HASH_METHOD_SSHA :
512 return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
513
514 case HASH_METHOD_MD5 :
515 case HASH_METHOD_SMD5 :
516 return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
517
518 case HASH_METHOD_CRYPT :
519 if ( salt == null )
520 {
521 salt = new byte[2];
522 SecureRandom sr = new SecureRandom();
523 int i1 = sr.nextInt( 64 );
524 int i2 = sr.nextInt( 64 );
525
526 salt[0] = ( byte ) ( i1 < 12 ? ( i1 + '.' ) : i1 < 38 ? ( i1 + 'A' - 12 ) : ( i1 + 'a' - 38 ) );
527 salt[1] = ( byte ) ( i2 < 12 ? ( i2 + '.' ) : i2 < 38 ? ( i2 + 'A' - 12 ) : ( i2 + 'a' - 38 ) );
528 }
529
530 String saltWithCrypted = UnixCrypt.crypt( StringTools.utf8ToString( credentials ),
531 StringTools.utf8ToString( salt ) );
532 String crypted = saltWithCrypted.substring( 2 );
533
534 return StringTools.getBytesUtf8( crypted );
535
536 default :
537 return credentials;
538 }
539 }
540
541
542 /**
543 * Local function which request the password from the backend
544 * @param principalDn the principal to lookup
545 * @return the credentials from the backend
546 * @throws NamingException if there are problems accessing backend
547 */
548 private byte[] lookupUserPassword( BindOperationContext opContext ) throws Exception
549 {
550 // ---- lookup the principal entry's userPassword attribute
551 ServerEntry userEntry;
552
553 try
554 {
555 /*
556 * NOTE: at this point the BindOperationContext does not has a
557 * null session since the user has not yet authenticated so we
558 * cannot use opContext.lookup() yet. This is a very special
559 * case where we cannot rely on the opContext to perform a new
560 * sub operation.
561 */
562 LookupOperationContext lookupContext =
563 new LookupOperationContext( getDirectoryService().getAdminSession(), opContext.getDn() );
564 lookupContext.setByPassed( USERLOOKUP_BYPASS );
565 userEntry = getDirectoryService().getOperationManager().lookup( lookupContext );
566
567 if ( userEntry == null )
568 {
569 LdapDN dn = opContext.getDn();
570 String upDn = ( dn == null ? "" : dn.getUpName() );
571
572 throw new LdapAuthenticationException( "Failed to lookup user for authentication: "
573 + upDn );
574 }
575 }
576 catch ( Exception cause )
577 {
578 LOG.error( "Authentication error : " + cause.getMessage() );
579 LdapAuthenticationException e = new LdapAuthenticationException( cause.getMessage() );
580 e.setRootCause( e );
581 throw e;
582 }
583
584 Value<?> userPassword;
585
586 EntryAttribute userPasswordAttr = userEntry.get( SchemaConstants.USER_PASSWORD_AT );
587
588 // ---- assert that credentials match
589 if ( userPasswordAttr == null )
590 {
591 return StringTools.EMPTY_BYTES;
592 }
593 else
594 {
595 userPassword = userPasswordAttr.get();
596
597 if ( userPassword instanceof ServerStringValue )
598 {
599 return StringTools.getBytesUtf8( (String)userPassword.get() );
600 }
601 else
602 {
603 return (byte[])userPassword.get();
604 }
605 }
606 }
607
608
609 /**
610 * Get the algorithm of a password, which is stored in the form "{XYZ}...".
611 * The method returns null, if the argument is not in this form. It returns
612 * XYZ, if XYZ is an algorithm known to the MessageDigest class of
613 * java.security.
614 *
615 * @param password a byte[]
616 * @return included message digest alorithm, if any
617 * @throws IllegalArgumentException if the algorithm cannot be identified
618 */
619 protected String getAlgorithmForHashedPassword( byte[] password ) throws IllegalArgumentException
620 {
621 String result = null;
622
623 // Check if password arg is string or byte[]
624 String sPassword = StringTools.utf8ToString( password );
625 int rightParen = sPassword.indexOf( '}' );
626
627 if ( ( sPassword.length() > 2 ) &&
628 ( sPassword.charAt( 0 ) == '{' ) &&
629 ( rightParen > -1 ) )
630 {
631 String algorithm = sPassword.substring( 1, rightParen );
632
633 if ( LdapSecurityConstants.HASH_METHOD_CRYPT.getName().equalsIgnoreCase( algorithm ) )
634 {
635 return algorithm;
636 }
637
638 try
639 {
640 MessageDigest.getInstance( algorithm );
641 result = algorithm;
642 }
643 catch ( NoSuchAlgorithmException e )
644 {
645 LOG.warn( "Unknown message digest algorithm in password: " + algorithm, e );
646 }
647 }
648
649 return result;
650 }
651
652
653 /**
654 * Creates a digested password. For a given hash algorithm and a password
655 * value, the algorithm is applied to the password, and the result is Base64
656 * encoded. The method returns a String which looks like "{XYZ}bbbbbbb",
657 * whereas XYZ is the name of the algorithm, and bbbbbbb is the Base64
658 * encoded value of XYZ applied to the password.
659 *
660 * @param algorithm
661 * an algorithm which is supported by
662 * java.security.MessageDigest, e.g. SHA
663 * @param password
664 * password value, a byte[]
665 *
666 * @return a digested password, which looks like
667 * {SHA}LhkDrSoM6qr0fW6hzlfOJQW61tc=
668 *
669 * @throws IllegalArgumentException
670 * if password is neither a String nor a byte[], or algorithm is
671 * not known to java.security.MessageDigest class
672 */
673 protected String createDigestedPassword( String algorithm, byte[] password ) throws IllegalArgumentException
674 {
675 // create message digest object
676 try
677 {
678 if ( LdapSecurityConstants.HASH_METHOD_CRYPT.getName().equalsIgnoreCase( algorithm ) )
679 {
680 String saltWithCrypted = UnixCrypt.crypt( StringTools.utf8ToString( password ), "" );
681 String crypted = saltWithCrypted.substring( 2 );
682 return '{' + algorithm + '}' + Arrays.toString( StringTools.getBytesUtf8( crypted ) );
683 }
684 else
685 {
686 MessageDigest digest = MessageDigest.getInstance( algorithm );
687
688 // calculate hashed value of password
689 byte[] fingerPrint = digest.digest( password );
690 char[] encoded = Base64.encode( fingerPrint );
691
692 // create return result of form "{alg}bbbbbbb"
693 return '{' + algorithm + '}' + new String( encoded );
694 }
695 }
696 catch ( NoSuchAlgorithmException nsae )
697 {
698 LOG.error( "Cannot create a digested password for algorithm '{}'", algorithm );
699 throw new IllegalArgumentException( nsae.getMessage() );
700 }
701 }
702
703
704 /**
705 * Remove the principal form the cache. This is used when the user changes
706 * his password.
707 */
708 public void invalidateCache( LdapDN bindDn )
709 {
710 synchronized( credentialCache )
711 {
712 credentialCache.remove( bindDn.getNormName() );
713 }
714 }
715 }