From 4a1ae7f29235f6db38befa01e7dac4c5cccfce06 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2016 23:11:48 -0700 Subject: [PATCH] GUACAMOLE-36: Verify new passwords against history. --- .../jdbc/security/PasswordPolicyService.java | 57 +++++++++++++++++++ .../security/PasswordReusedException.java | 52 +++++++++++++++++ .../src/main/resources/translations/en.json | 1 + 3 files changed, 110 insertions(+) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java index 2b6c74cce..d6a9fe575 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java @@ -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 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 } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java new file mode 100644 index 000000000..3e3ad9b1e --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java @@ -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) + )); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json index 95bb59b58..78728c3c6 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json @@ -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."