001/* 002 * Copyright 2025 The Apache Software Foundation. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.apache.wiki.auth; 017 018import java.text.MessageFormat; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Properties; 022import java.util.ResourceBundle; 023import org.apache.wiki.api.core.Context; 024import org.apache.wiki.i18n.InternationalizationManager; 025 026/** 027 * a simple password complexity checker 028 * 029 * @since 3.0.0 030 */ 031public final class PasswordComplexityVerifier { 032 033 private PasswordComplexityVerifier() { 034 } 035 036 /** 037 * validates a password, returns a list of size 0 is all the checks 038 * pass.list of size > 0 = password is invalid with each item in the list 039 * containing the rule that failed.suitable to direct display to the user. 040 * i.e. the password was too short, or has too many repeating characters, 041 * etc. 042 * 043 * @param pwd 044 * @param context 045 * @return see above 046 */ 047 public static List<String> validate(String pwd, String previousPwd, Context context) { 048 Properties wikiProps = context.getEngine().getWikiProperties(); 049 final ResourceBundle rb = ResourceBundle.getBundle(InternationalizationManager.CORE_BUNDLE, context.getWikiSession().getLocale()); 050 051 int minLength = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.length.min", "8")); 052 int maxLength = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.length.max", "64")); 053 int minUpper = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.minUpper", "1")); 054 int minLower = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.minLower", "1")); 055 int minDigits = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.minDigits", "1")); 056 int minSymbols = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.minSymbols", "1")); 057 int maxRepeats = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.repeatingCharacters", "1")); 058 int minChanged = Integer.parseInt(wikiProps.getProperty("jspwiki.credentials.minChanged", "0")); 059 //potential future enhancement, detect common patterns like keyboard walks, asdf, etc 060 //boolean allowCommonPatterns = "true".equalsIgnoreCase(wikiProps.getProperty("jspwiki.credentials.allowCommonPatterns", "false")); 061 //potential future enhancements, detect common numerical patterns, such as 1234 oe 4321 062 //boolean allowSequentialNumbers = "true".equalsIgnoreCase(wikiProps.getProperty("jspwiki.credentials.allowSequentialNumberPatterns", "false")); 063 //perhaps a regex pattern can be added in the future 064 065 List<String> problems = new ArrayList<>(); 066 if (pwd == null) { 067 problems.add(MessageFormat.format(rb.getString("pwdcheck.tooshort"), minLength)); 068 return problems; 069 } 070 if (pwd.length() > maxLength) { 071 problems.add(MessageFormat.format(rb.getString("pwdcheck.toolong"), maxLength)); 072 } 073 if (pwd.length() < minLength) { 074 problems.add(MessageFormat.format(rb.getString("pwdcheck.tooshort"), minLength)); 075 } 076 char[] cred = pwd.toCharArray(); 077 int qtyChanged = 0; 078 int upper = 0; 079 int lower = 0; 080 int digits = 0; 081 int other = 0; 082 //the higest number of repeats 083 int repeats = 0; 084 int localrepeats = 0; 085 boolean repeatCheck = false; 086 for (int i = 0; i < cred.length; i++) { 087 if (Character.isDigit(cred[i])) { 088 digits++; 089 } else if (Character.isUpperCase(cred[i])) { 090 upper++; 091 } else if (Character.isLowerCase(cred[i])) { 092 lower++; 093 } else { 094 other++; 095 } 096 if (i > 0) { 097 if (cred[i] == cred[i - 1]) { 098 //ok we have a repeat 099 if (repeatCheck) { 100 //existing sequence 101 localrepeats++; 102 } else { 103 //this is a new sequence 104 repeatCheck = true; 105 localrepeats = 1; 106 } 107 108 } else { 109 repeatCheck = false; 110 if (localrepeats > repeats) { 111 repeats = localrepeats; 112 } 113 localrepeats = 0; 114 } 115 } 116 } 117 118 //quantity of changed character test 119 if (minChanged > 0 && previousPwd != null) { 120 for (int i = 0; i < cred.length; i++) { 121 if ((i + 1) < previousPwd.length()) { 122 if (previousPwd.charAt(i) != cred[i]) { 123 qtyChanged++; 124 } 125 } else { 126 //i.e. the new pass is longer than the old one 127 break; 128 } 129 130 } 131 if (qtyChanged < minChanged) { 132 problems.add(MessageFormat.format(rb.getString("pwdcheck.minchanged"), minChanged)); 133 } 134 } 135 136 if (repeatCheck) { 137 if (localrepeats > repeats) { 138 repeats = localrepeats; 139 } 140 } 141 142 if (maxRepeats > 0 && repeats > maxRepeats) { 143 problems.add(MessageFormat.format(rb.getString("pwdcheck.repeats"), maxRepeats)); 144 } 145 if (minUpper > 0 && upper < minUpper) { 146 problems.add(MessageFormat.format(rb.getString("pwdcheck.minUpper"), minUpper)); 147 } 148 if (minLower > 0 && lower < minLower) { 149 problems.add(MessageFormat.format(rb.getString("pwdcheck.minLower"), minUpper)); 150 } 151 if (minDigits > 0 && digits < minDigits) { 152 problems.add(MessageFormat.format(rb.getString("pwdcheck.minDigits"), minUpper)); 153 } 154 if (minSymbols > 0 && other < minSymbols) { 155 problems.add(MessageFormat.format(rb.getString("pwdcheck.minOther"), minSymbols)); 156 } 157 return problems; 158 159 } 160}