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 17:32:04 +0200 (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 }