View Javadoc

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.changepw.service;
21  
22  
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import javax.security.auth.kerberos.KerberosPrincipal;
27  
28  import org.apache.directory.server.changepw.ChangePasswordServer;
29  import org.apache.directory.server.changepw.exceptions.ChangePasswordException;
30  import org.apache.directory.server.changepw.exceptions.ErrorType;
31  import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
32  import org.apache.mina.common.IoSession;
33  import org.apache.mina.handler.chain.IoHandlerCommand;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  
38  /**
39   * A basic password policy check using well-established methods.
40   * 
41   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
42   * @version $Rev: 583938 $, $Date: 2007-10-11 21:57:20 +0200 (Do, 11 Okt 2007) $
43   */
44  public class CheckPasswordPolicy implements IoHandlerCommand
45  {
46      /** the log for this class */
47      private static final Logger log = LoggerFactory.getLogger( CheckPasswordPolicy.class );
48  
49      private String contextKey = "context";
50  
51  
52      public void execute( NextCommand next, IoSession session, Object message ) throws Exception
53      {
54          ChangePasswordContext changepwContext = ( ChangePasswordContext ) session.getAttribute( getContextKey() );
55  
56          ChangePasswordServer config = changepwContext.getConfig();
57          Authenticator authenticator = changepwContext.getAuthenticator();
58          KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
59  
60          String password = changepwContext.getPassword();
61          String username = clientPrincipal.getName();
62  
63          int passwordLength = config.getPasswordLengthPolicy();
64          int categoryCount = config.getCategoryCountPolicy();
65          int tokenSize = config.getTokenSizePolicy();
66  
67          if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) )
68          {
69              String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize );
70              log.error( explanation );
71  
72              byte[] explanatoryData = explanation.getBytes( "UTF-8" );
73  
74              throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, explanatoryData );
75          }
76  
77          next.execute( session, message );
78      }
79  
80  
81      /**
82       * Tests that:
83       * The password is at least six characters long.
84       * The password contains a mix of characters.
85       * The password does not contain three letter (or more) tokens from the user's account name.
86       */
87      boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize )
88      {
89          return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount )
90              && isValidUsernameSubstring( username, password, tokenSize );
91      }
92  
93  
94      /**
95       * The password is at least six characters long.
96       */
97      boolean isValidPasswordLength( String password, int passwordLength )
98      {
99          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 }