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