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}