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 }