blob: c8e09c84180344d924f9f1e0b8dfbca400f7ae32 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.directory.server.changepw.service;
import java.util.ArrayList;
import java.util.List;
import javax.security.auth.kerberos.KerberosPrincipal;
import org.apache.directory.server.changepw.ChangePasswordServer;
import org.apache.directory.server.changepw.exceptions.ChangePasswordException;
import org.apache.directory.server.changepw.exceptions.ErrorType;
import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
import org.apache.mina.common.IoSession;
import org.apache.mina.handler.chain.IoHandlerCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A basic password policy check using well-established methods.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$, $Date$
*/
public class CheckPasswordPolicy implements IoHandlerCommand
{
/** the log for this class */
private static final Logger log = LoggerFactory.getLogger( CheckPasswordPolicy.class );
private String contextKey = "context";
public void execute( NextCommand next, IoSession session, Object message ) throws Exception
{
ChangePasswordContext changepwContext = ( ChangePasswordContext ) session.getAttribute( getContextKey() );
ChangePasswordServer config = changepwContext.getConfig();
Authenticator authenticator = changepwContext.getAuthenticator();
KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
String password = changepwContext.getPassword();
String username = clientPrincipal.getName();
int passwordLength = config.getPasswordLengthPolicy();
int categoryCount = config.getCategoryCountPolicy();
int tokenSize = config.getTokenSizePolicy();
if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) )
{
String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize );
log.error( explanation );
byte[] explanatoryData = explanation.getBytes( "UTF-8" );
throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, explanatoryData );
}
next.execute( session, message );
}
/**
* Tests that:
* The password is at least six characters long.
* The password contains a mix of characters.
* The password does not contain three letter (or more) tokens from the user's account name.
*/
boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize )
{
return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount )
&& isValidUsernameSubstring( username, password, tokenSize );
}
/**
* The password is at least six characters long.
*/
boolean isValidPasswordLength( String password, int passwordLength )
{
return password.length() >= passwordLength;
}
/**
* The password contains characters from at least three of the following four categories:
* English uppercase characters (A - Z)
* English lowercase characters (a - z)
* Base 10 digits (0 - 9)
* Any non-alphanumeric character (for example: !, $, #, or %)
*/
boolean isValidCategoryCount( String password, int categoryCount )
{
int uppercase = 0;
int lowercase = 0;
int digit = 0;
int nonAlphaNumeric = 0;
char[] characters = password.toCharArray();
for ( int ii = 0; ii < characters.length; ii++ )
{
if ( Character.isLowerCase( characters[ii] ) )
{
lowercase = 1;
}
else
{
if ( Character.isUpperCase( characters[ii] ) )
{
uppercase = 1;
}
else
{
if ( Character.isDigit( characters[ii] ) )
{
digit = 1;
}
else
{
if ( !Character.isLetterOrDigit( characters[ii] ) )
{
nonAlphaNumeric = 1;
}
}
}
}
}
return ( uppercase + lowercase + digit + nonAlphaNumeric ) >= categoryCount;
}
/**
* The password does not contain three letter (or more) tokens from the user's account name.
*
* If the account name is less than three characters long, this check is not performed
* because the rate at which passwords would be rejected is too high. For each token that is
* three or more characters long, that token is searched for in the password; if it is present,
* the password change is rejected. For example, the name "First M. Last" would be split into
* three tokens: "First", "M", and "Last". Because the second token is only one character long,
* it would be ignored. Therefore, this user could not have a password that included either
* "first" or "last" as a substring anywhere in the password. All of these checks are
* case-insensitive.
*/
boolean isValidUsernameSubstring( String username, String password, int tokenSize )
{
String[] tokens = username.split( "[^a-zA-Z]" );
for ( int ii = 0; ii < tokens.length; ii++ )
{
if ( tokens[ii].length() >= tokenSize )
{
if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) )
{
return false;
}
}
}
return true;
}
private String buildErrorMessage( String username, String password, int passwordLength, int categoryCount,
int tokenSize )
{
List<String> violations = new ArrayList<String>();
if ( !isValidPasswordLength( password, passwordLength ) )
{
violations.add( "length too short" );
}
if ( !isValidCategoryCount( password, categoryCount ) )
{
violations.add( "insufficient character mix" );
}
if ( !isValidUsernameSubstring( username, password, tokenSize ) )
{
violations.add( "contains portions of username" );
}
StringBuffer sb = new StringBuffer( "Password violates policy: " );
boolean isFirst = true;
for ( String violation : violations )
{
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( ", " );
}
sb.append( violation );
}
return sb.toString();
}
protected String getContextKey()
{
return ( this.contextKey );
}
}