    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}