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.changepw.service;
021    
022    
023    import java.util.ArrayList;
024    import java.util.List;
025    
026    import javax.security.auth.kerberos.KerberosPrincipal;
027    
028    import org.apache.directory.server.changepw.ChangePasswordServer;
029    import org.apache.directory.server.changepw.exceptions.ChangePasswordException;
030    import org.apache.directory.server.changepw.exceptions.ErrorType;
031    import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
032    import org.apache.mina.core.session.IoSession;
033    import org.apache.mina.handler.chain.IoHandlerCommand;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    
038    /**
039     * A basic password policy check using well-established methods.
040     * 
041     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
042     * @version $Rev: 725712 $, $Date: 2008-12-11 16:32:04 +0100 (Thu, 11 Dec 2008) $
043     */
044    public class CheckPasswordPolicy implements IoHandlerCommand
045    {
046        /** the log for this class */
047        private static final Logger log = LoggerFactory.getLogger( CheckPasswordPolicy.class );
048    
049        private String contextKey = "context";
050    
051    
052        public void execute( NextCommand next, IoSession session, Object message ) throws Exception
053        {
054            ChangePasswordContext changepwContext = ( ChangePasswordContext ) session.getAttribute( getContextKey() );
055    
056            ChangePasswordServer config = changepwContext.getConfig();
057            Authenticator authenticator = changepwContext.getAuthenticator();
058            KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
059    
060            String password = changepwContext.getPassword();
061            String username = clientPrincipal.getName();
062    
063            int passwordLength = config.getPasswordLengthPolicy();
064            int categoryCount = config.getCategoryCountPolicy();
065            int tokenSize = config.getTokenSizePolicy();
066    
067            if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) )
068            {
069                String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize );
070                log.error( explanation );
071    
072                byte[] explanatoryData = explanation.getBytes( "UTF-8" );
073    
074                throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, explanatoryData );
075            }
076    
077            next.execute( session, message );
078        }
079    
080    
081        /**
082         * Tests that:
083         * The password is at least six characters long.
084         * The password contains a mix of characters.
085         * The password does not contain three letter (or more) tokens from the user's account name.
086         */
087        boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize )
088        {
089            return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount )
090                && isValidUsernameSubstring( username, password, tokenSize );
091        }
092    
093    
094        /**
095         * The password is at least six characters long.
096         */
097        boolean isValidPasswordLength( String password, int passwordLength )
098        {
099            return password.length() >= passwordLength;
100        }
101    
102    
103        /**
104         * The password contains characters from at least three of the following four categories:
105         * English uppercase characters (A - Z)
106         * English lowercase characters (a - z)
107         * Base 10 digits (0 - 9)
108         * Any non-alphanumeric character (for example: !, $, #, or %)
109         */
110        boolean isValidCategoryCount( String password, int categoryCount )
111        {
112            int uppercase = 0;
113            int lowercase = 0;
114            int digit = 0;
115            int nonAlphaNumeric = 0;
116    
117            char[] characters = password.toCharArray();
118    
119            for ( int ii = 0; ii < characters.length; ii++ )
120            {
121                if ( Character.isLowerCase( characters[ii] ) )
122                {
123                    lowercase = 1;
124                }
125                else
126                {
127                    if ( Character.isUpperCase( characters[ii] ) )
128                    {
129                        uppercase = 1;
130                    }
131                    else
132                    {
133                        if ( Character.isDigit( characters[ii] ) )
134                        {
135                            digit = 1;
136                        }
137                        else
138                        {
139                            if ( !Character.isLetterOrDigit( characters[ii] ) )
140                            {
141                                nonAlphaNumeric = 1;
142                            }
143                        }
144                    }
145                }
146            }
147    
148            return ( uppercase + lowercase + digit + nonAlphaNumeric ) >= categoryCount;
149        }
150    
151    
152        /**
153         * The password does not contain three letter (or more) tokens from the user's account name.
154         * 
155         * If the account name is less than three characters long, this check is not performed
156         * because the rate at which passwords would be rejected is too high. For each token that is
157         * three or more characters long, that token is searched for in the password; if it is present,
158         * the password change is rejected. For example, the name "First M. Last" would be split into
159         * three tokens: "First", "M", and "Last". Because the second token is only one character long,
160         * it would be ignored. Therefore, this user could not have a password that included either
161         * "first" or "last" as a substring anywhere in the password. All of these checks are
162         * case-insensitive.
163         */
164        boolean isValidUsernameSubstring( String username, String password, int tokenSize )
165        {
166            String[] tokens = username.split( "[^a-zA-Z]" );
167    
168            for ( int ii = 0; ii < tokens.length; ii++ )
169            {
170                if ( tokens[ii].length() >= tokenSize )
171                {
172                    if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) )
173                    {
174                        return false;
175                    }
176                }
177            }
178    
179            return true;
180        }
181    
182    
183        private String buildErrorMessage( String username, String password, int passwordLength, int categoryCount,
184            int tokenSize )
185        {
186            List<String> violations = new ArrayList<String>();
187    
188            if ( !isValidPasswordLength( password, passwordLength ) )
189            {
190                violations.add( "length too short" );
191            }
192    
193            if ( !isValidCategoryCount( password, categoryCount ) )
194            {
195                violations.add( "insufficient character mix" );
196            }
197    
198            if ( !isValidUsernameSubstring( username, password, tokenSize ) )
199            {
200                violations.add( "contains portions of username" );
201            }
202    
203            StringBuffer sb = new StringBuffer( "Password violates policy:  " );
204    
205            boolean isFirst = true;
206    
207            for ( String violation : violations )
208            {
209                if ( isFirst )
210                {
211                    isFirst = false;
212                }
213                else
214                {
215                    sb.append( ", " );
216                }
217    
218                sb.append( violation );
219            }
220    
221            return sb.toString();
222        }
223    
224    
225        protected String getContextKey()
226        {
227            return ( this.contextKey );
228        }
229    }