GUACAMOLE-36: Verify new passwords against history.

This commit is contained in:
Michael Jumper
2016-08-22 23:11:48 -07:00
parent a943077d40
commit 4a1ae7f292
3 changed files with 110 additions and 0 deletions

View File

@@ -20,6 +20,8 @@
package org.apache.guacamole.auth.jdbc.security;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -49,6 +51,12 @@ public class PasswordPolicyService {
@Inject
private PasswordRecordMapper passwordRecordMapper;
/**
* Service for hashing passwords.
*/
@Inject
private PasswordEncryptionService encryptionService;
/**
* Regular expression which matches only if the string contains at least one
* lowercase character.
@@ -105,6 +113,49 @@ public class PasswordPolicyService {
}
/**
* Returns whether the given password matches any of the user's previous
* passwords. Regardless of the value specified here, the maximum number of
* passwords involved in this check depends on how many previous passwords
* were actually recorded, which depends on the password policy.
*
* @param password
* The password to check.
*
* @param username
* The username of the user whose history should be compared against
* the given password.
*
* @param historySize
* The maximum number of history records to compare the password
* against.
*
* @return
* true if the given password matches any of the user's previous
* passwords, up to the specified limit, false otherwise.
*/
private boolean matchesPreviousPasswords(String password, String username,
int historySize) {
// No need to compare if no history is relevant
if (historySize <= 0)
return false;
// Check password against all recorded hashes
List<PasswordRecordModel> history = passwordRecordMapper.select(username, historySize);
for (PasswordRecordModel record : history) {
byte[] hash = encryptionService.createPasswordHash(password, record.getPasswordSalt());
if (Arrays.equals(hash, record.getPasswordHash()))
return true;
}
// No passwords match
return false;
}
/**
* Verifies that the given new password complies with the password policy
* configured within guacamole.properties, throwing a GuacamoleException if
@@ -152,6 +203,12 @@ public class PasswordPolicyService {
throw new PasswordRequiresSymbolException(
"Passwords must contain at least one non-alphanumeric character.");
// Prohibit password reuse
int historySize = policy.getHistorySize();
if (matchesPreviousPasswords(password, username, historySize))
throw new PasswordReusedException(
"Password matches a previously-used password.", historySize);
// Password passes all defined restrictions
}

View File

@@ -0,0 +1,52 @@
/*
* 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.guacamole.auth.jdbc.security;
import java.util.Collections;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Thrown when an attempt is made to reuse a previous password, in violation of
* the defined password policy.
*
* @author Michael Jumper
*/
public class PasswordReusedException extends PasswordPolicyException {
/**
* Creates a new PasswordReusedException with the given human-readable
* message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*
* @param historySize
* The number of previous passwords which are remembered for each user,
* and must not be reused.
*/
public PasswordReusedException(String message, int historySize) {
super(message, new TranslatableMessage(
"PASSWORD_POLICY.ERROR_REUSED",
Collections.singletonMap("HISTORY_SIZE", historySize)
));
}
}

View File

@@ -60,6 +60,7 @@
"ERROR_REQUIRES_DIGIT" : "Passwords must contain at least one digit.",
"ERROR_REQUIRES_MULTIPLE_CASE" : "Passwords must contain both uppercase and lowercase characters.",
"ERROR_REQUIRES_NON_ALNUM" : "Passwords must contain at least one symbol.",
"ERROR_REUSED" : "This password has already been used. Please do not reuse any of the previous {HISTORY_SIZE} {HISTORY_SIZE, plural, one{password} other{passwords}}.",
"ERROR_TOO_SHORT" : "Passwords must be at least {LENGTH} {LENGTH, plural, one{character} other{characters}} long.",
"ERROR_TOO_YOUNG" : "The password for this account has already been reset. Please wait at least {WAIT} more {WAIT, plural, one{day} other{days}} before changing the password again."