1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20 package org.apache.guacamole.auth.jdbc.security; 21 22 import com.google.inject.Inject; 23 import java.util.Arrays; 24 import java.util.List; 25 import java.util.concurrent.TimeUnit; 26 import java.util.regex.Matcher; 27 import java.util.regex.Pattern; 28 import org.apache.guacamole.GuacamoleException; 29 import org.apache.guacamole.auth.jdbc.JDBCEnvironment; 30 import org.apache.guacamole.auth.jdbc.user.ModeledUser; 31 import org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper; 32 import org.apache.guacamole.auth.jdbc.user.PasswordRecordModel; 33 34 /** 35 * Service which verifies compliance with the password policy configured via 36 * guacamole.properties. 37 */ 38 public class PasswordPolicyService { 39 40 /** 41 * The Guacamole server environment. 42 */ 43 @Inject 44 private JDBCEnvironment environment; 45 46 /** 47 * Mapper for creating/retrieving previously-set passwords. 48 */ 49 @Inject 50 private PasswordRecordMapper passwordRecordMapper; 51 52 /** 53 * Service for hashing passwords. 54 */ 55 @Inject 56 private PasswordEncryptionService encryptionService; 57 58 /** 59 * Regular expression which matches only if the string contains at least one 60 * lowercase character. 61 */ 62 private final Pattern CONTAINS_LOWERCASE = Pattern.compile("\\p{javaLowerCase}"); 63 64 /** 65 * Regular expression which matches only if the string contains at least one 66 * uppercase character. 67 */ 68 private final Pattern CONTAINS_UPPERCASE = Pattern.compile("\\p{javaUpperCase}"); 69 70 /** 71 * Regular expression which matches only if the string contains at least one 72 * numeric character. 73 */ 74 private final Pattern CONTAINS_DIGIT = Pattern.compile("\\p{Digit}"); 75 76 /** 77 * Regular expression which matches only if the string contains at least one 78 * non-alphanumeric character. 79 */ 80 private final Pattern CONTAINS_NON_ALPHANUMERIC = 81 Pattern.compile("[^\\p{javaLowerCase}\\p{javaUpperCase}\\p{Digit}]"); 82 83 /** 84 * Returns whether the given string matches all of the provided regular 85 * expressions. 86 * 87 * @param str 88 * The string to test against all provided regular expressions. 89 * 90 * @param patterns 91 * The regular expressions to match against the given string. 92 * 93 * @return 94 * true if the given string matches all provided regular expressions, 95 * false otherwise. 96 */ matches(String str, Pattern... patterns)97 private boolean matches(String str, Pattern... patterns) { 98 99 // Check given string against all provided patterns 100 for (Pattern pattern : patterns) { 101 102 // Fail overall test if any pattern fails to match 103 Matcher matcher = pattern.matcher(str); 104 if (!matcher.find()) 105 return false; 106 107 } 108 109 // All provided patterns matched 110 return true; 111 112 } 113 114 /** 115 * Returns whether the given password matches any of the user's previous 116 * passwords. Regardless of the value specified here, the maximum number of 117 * passwords involved in this check depends on how many previous passwords 118 * were actually recorded, which depends on the password policy. 119 * 120 * @param password 121 * The password to check. 122 * 123 * @param username 124 * The username of the user whose history should be compared against 125 * the given password. 126 * 127 * @param historySize 128 * The maximum number of history records to compare the password 129 * against. 130 * 131 * @return 132 * true if the given password matches any of the user's previous 133 * passwords, up to the specified limit, false otherwise. 134 */ matchesPreviousPasswords(String password, String username, int historySize)135 private boolean matchesPreviousPasswords(String password, String username, 136 int historySize) { 137 138 // No need to compare if no history is relevant 139 if (historySize <= 0) 140 return false; 141 142 // Check password against all recorded hashes 143 List<PasswordRecordModel> history = passwordRecordMapper.select(username, historySize); 144 for (PasswordRecordModel record : history) { 145 146 byte[] hash = encryptionService.createPasswordHash(password, record.getPasswordSalt()); 147 if (Arrays.equals(hash, record.getPasswordHash())) 148 return true; 149 150 } 151 152 // No passwords match 153 return false; 154 155 } 156 157 /** 158 * Verifies that the given new password complies with the password policy 159 * configured within guacamole.properties, throwing a GuacamoleException if 160 * the policy is violated in any way. 161 * 162 * @param username 163 * The username of the user whose password is being changed. 164 * 165 * @param password 166 * The proposed new password. 167 * 168 * @throws GuacamoleException 169 * If the password policy cannot be parsed, or if the proposed password 170 * violates the password policy. 171 */ verifyPassword(String username, String password)172 public void verifyPassword(String username, String password) 173 throws GuacamoleException { 174 175 // Retrieve password policy from environment 176 PasswordPolicy policy = environment.getPasswordPolicy(); 177 178 // Enforce minimum password length 179 if (password.length() < policy.getMinimumLength()) 180 throw new PasswordMinimumLengthException( 181 "Password does not meet minimum length requirements.", 182 policy.getMinimumLength()); 183 184 // Disallow passwords containing the username 185 if (policy.isUsernameProhibited() && password.toLowerCase().contains(username.toLowerCase())) 186 throw new PasswordContainsUsernameException( 187 "Password must not contain username."); 188 189 // Require both uppercase and lowercase characters 190 if (policy.isMultipleCaseRequired() && !matches(password, CONTAINS_LOWERCASE, CONTAINS_UPPERCASE)) 191 throw new PasswordRequiresMultipleCaseException( 192 "Password must contain both uppercase and lowercase."); 193 194 // Require digits 195 if (policy.isNumericRequired() && !matches(password, CONTAINS_DIGIT)) 196 throw new PasswordRequiresDigitException( 197 "Passwords must contain at least one digit."); 198 199 // Require non-alphanumeric symbols 200 if (policy.isNonAlphanumericRequired() && !matches(password, CONTAINS_NON_ALPHANUMERIC)) 201 throw new PasswordRequiresSymbolException( 202 "Passwords must contain at least one non-alphanumeric character."); 203 204 // Prohibit password reuse 205 int historySize = policy.getHistorySize(); 206 if (matchesPreviousPasswords(password, username, historySize)) 207 throw new PasswordReusedException( 208 "Password matches a previously-used password.", historySize); 209 210 // Password passes all defined restrictions 211 212 } 213 214 /** 215 * Returns the age of the given user's password, in days. The age of a 216 * user's password is the amount of time elapsed since the password was last 217 * changed or reset. 218 * 219 * @param user 220 * The user to calculate the password age of. 221 * 222 * @return 223 * The age of the given user's password, in days. 224 */ getPasswordAge(ModeledUser user)225 private long getPasswordAge(ModeledUser user) { 226 227 // If no password was set, then no time has elapsed 228 PasswordRecordModel passwordRecord = user.getPasswordRecord(); 229 if (passwordRecord == null) 230 return 0; 231 232 // Pull both current time and the time the password was last reset 233 long currentTime = System.currentTimeMillis(); 234 long lastResetTime = passwordRecord.getPasswordDate().getTime(); 235 236 // Calculate the number of days elapsed since the password was last reset 237 return TimeUnit.DAYS.convert(currentTime - lastResetTime, TimeUnit.MILLISECONDS); 238 239 } 240 241 /** 242 * Verifies that the given user can change their password without violating 243 * password aging policy. If changing the user's password would violate the 244 * aging policy, a GuacamoleException will be thrown. 245 * 246 * @param user 247 * The user whose password is changing. 248 * 249 * @throws GuacamoleException 250 * If the user's password cannot be changed due to the password aging 251 * policy, or of the password policy cannot be parsed from 252 * guacamole.properties. 253 */ verifyPasswordAge(ModeledUser user)254 public void verifyPasswordAge(ModeledUser user) throws GuacamoleException { 255 256 // Retrieve password policy from environment 257 PasswordPolicy policy = environment.getPasswordPolicy(); 258 259 long minimumAge = policy.getMinimumAge(); 260 long passwordAge = getPasswordAge(user); 261 262 // Require that sufficient time has elapsed before allowing the password 263 // to be changed 264 if (passwordAge < minimumAge) 265 throw new PasswordTooYoungException("Password was already recently changed.", 266 minimumAge - passwordAge); 267 268 } 269 270 /** 271 * Returns whether the given user's password is expired due to the password 272 * aging policy. 273 * 274 * @param user 275 * The user to check. 276 * 277 * @return 278 * true if the user needs to change their password to comply with the 279 * password aging policy, false otherwise. 280 * 281 * @throws GuacamoleException 282 * If the password policy cannot be parsed. 283 */ isPasswordExpired(ModeledUser user)284 public boolean isPasswordExpired(ModeledUser user) 285 throws GuacamoleException { 286 287 // Retrieve password policy from environment 288 PasswordPolicy policy = environment.getPasswordPolicy(); 289 290 // There is no maximum password age if 0 291 int maxPasswordAge = policy.getMaximumAge(); 292 if (maxPasswordAge == 0) 293 return false; 294 295 // Determine whether password is expired based on maximum age 296 return getPasswordAge(user) >= maxPasswordAge; 297 298 } 299 300 /** 301 * Records the password that was associated with the given user at the time 302 * the user was queried, such that future attempts to set that same password 303 * for that user will be denied. The number of passwords remembered for each 304 * user is limited by the password policy. 305 * 306 * @param user 307 * The user whose password should be recorded within the password 308 * history. 309 * 310 * @throws GuacamoleException 311 * If the password policy cannot be parsed. 312 */ recordPassword(ModeledUser user)313 public void recordPassword(ModeledUser user) 314 throws GuacamoleException { 315 316 // Retrieve password policy from environment 317 PasswordPolicy policy = environment.getPasswordPolicy(); 318 319 // Nothing to do if history is not being recorded 320 int historySize = policy.getHistorySize(); 321 if (historySize <= 0) 322 return; 323 324 // Store previous password in history 325 passwordRecordMapper.insert(user.getPasswordRecord(), historySize); 326 327 } 328 329 } 330