2

I am trying to create a java to check strength of a password with regex. The password must pass 3 out of 4 conditions:

  1. lowercase
  2. uppercase
  3. contains digits
  4. has special characters

The code looks like below:

import java.util.*;

public class StringPasswordStrength {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter password: ");
        String password = input.nextLine();
        boolean test = checkStrength(password);
        if (test) {
            System.out.print("OK");
        }
        else {
            System.out.print("Not strong enough!");
        }       
    }
    public static boolean checkStrength(String password) {
        if (password.matches("^(?=.*[a-zA-Z][0-9][!@#$%^&*])(?=.{8,})")){
            return true;
        }
        else {
            return false;
        }
    }
}

However when the password is Passw0rd it doesn't accept. How can I change the conditions in regex that the program would accept Passw0rd because it passes 3 out of 4 conditions: uppercase, lowercase and digit?

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
Trung
  • 51
  • 1
  • 7
  • I wrote an answer. However, I realized you never mentioned the 4th requirement, which may require a different tool to implement. So I didn't post it. Please update your post to include all relevant information, so those answering know exactly what your problem is. In general terms, you can create a list of the requirements (4 elements), & pop elements as they are found. If the list only contains 1 element, that means 3 of the 4 elements were met. – Vince Feb 28 '20 at 02:50
  • Your question is not clear. Please edit to state in words the four requirements you are testing for. Your regular expressions suggests there are only two requirements: there is a sequence of a letter followed by a digit followed by a special character; and the string is at least 8 characters long. Considering the nature of password requirements I am guessing that three of the following four are required: a letter, digit, special character and at least eight characters. Am I correct? – Cary Swoveland Feb 28 '20 at 08:10

5 Answers5

2

I would suggest avoiding a potentially cryptic regular expression for this, and instead to provide something easier to read, understand and maintain (albeit more verbose).

Something like this (depending on what your conditions are, of course). For example, a length test should be mandatory:

public boolean isValid(String password) {
    // password must contain 3 out of 4 of lowercase, uppercase, digits,
    // and others (punctuation, symbols, spaces, etc.).
    if (password == null) {
        return false;
    }

    if (password.length() < 8) {
        return false;
    }

    char[] chars = password.toCharArray();
    int lowers = 0;
    int uppers = 0;
    int digits = 0;
    int others = 0;
    for (Character c : chars) {
        if (Character.isLowerCase(c)) {
            lowers = 1;
        } else if (Character.isUpperCase(c)) {
            uppers = 1;
        } else if (Character.isDigit(c)) {
            digits = 1;
        } else {
            others = 1;
        }
    }
    // at least 3 out of 4 tests must pass:
    return (lowers + uppers + digits + others >= 3);
}

I understand this is not a direct answer to your question, and so may not meet your needs.

I am also deliberately avoiding the discussion about char[] vs. String for password handling in Java - for example, see this post.

EDIT: Removed wording relating to password length, and changed related code to reflect the question.

andrewJames
  • 19,570
  • 8
  • 19
  • 51
  • I edited only to be able to retract my downvote. My bad... – vefthym Feb 28 '20 at 01:20
  • can we somehow make conditions: uppercase, lowercase and digit must be included and special characters is optional. Like using | as "or" in condition? – Trung Feb 28 '20 at 01:36
  • You could make special characters optional by not checking for them? – BeUndead Feb 28 '20 at 01:40
  • sorry, i forgot. The program will check every possible cases using 3 out of 4 conditions. – Trung Feb 28 '20 at 01:45
  • Why did you force a requirement of the password being 15 characters long? That's crazy. Not only was that requirement not mentioned in the OP's post, but what site have you seen 15 characters being the minimum limit? – Vince Feb 28 '20 at 02:54
  • @andrewjames As with all programming problems, it's context specific. You're right, add a 15 char limit theory on top of the "change every 30 days to refresh the DB incase a leak occurred", passwords become a burden, giving higher risk of users to use incremental passwords (MyPassword1, MyPassword2, etc..). This would be a problem if a DB were to leak, as passwords would become more easily guessed if an old password was rainbow-tabled. If I were putting a password on a computer with no network card, just to avoid my kids from accessing, I would not use a 15 character password, that's for sure. – Vince Feb 28 '20 at 03:14
  • I'd +1 for encouraging readable code over compact code. But introducing semantics that the OP didn't request, without giving any context as to *why* you added it to the post, is making me hesitate. You shouldn't blindly enforce rules, especially stating they're "non-negotiable", could be harmful. – Vince Feb 28 '20 at 03:26
1

You could define a set of rules (regex), count how many a given password comply with and compare with the minimum you require. A possible implementation could be:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/**
 * Patterns to be tested in your passwords. If you want some of them
 * mandatory, you can define them in a "mandatoryPatterns" list and
 * check those ones always.
 */
static List<String> patterns = Arrays.asList(
        ".*[A-Z]+.*",
        ".*[a-z]+.*"
);

/** Number of required patterns. */
static long requiredPatterns = 1;

/** This functions counts the number of patterns that a password matches. */
static long passwordStrength(String password) {
    return patterns.stream().filter(password::matches).count();
}

static boolean checkStrength(String password) {
    return passwordStrength(password) >= requiredPatterns;
}

Stream.of("", "foo", "BAR", "FooBar").forEach(pass -> {
    System.out.println(pass);
    System.out.println(passwordStrength(pass));
    System.out.println(checkStrength(pass));
});
jaguililla
  • 1,916
  • 18
  • 21
1

Your issue has been pointed out by another user, along with a solution. This is an alternative solution.


Have 4 Pattern objects, one for each requirement

Pattern uppercase = Pattern.compile("[A-Z]");
Pattern number = Pattern.compile("\\d+");
Pattern symbol = Pattern.compile("[+&$%!@]");
Pattern other = ...;

String#matches "compiles" the regex every time it is called, which can be time consuming. By using Pattern objects, you'll be using already-compiled regex patterns.

Add the requirements to a list

List<Pattern> requirements = Arrays.asList(uppercase, number, symbol, other);

Loop over the list of requirements. For each requirement, check if the password matches the requirement. If the element does, increase a counter which tracks how many requirements have already been met.

If the requirements equals 3 (or is greater than 3), return true. If the loop exits gracefully, that means 3 requirements were not met; return false if the loop exits gracefully.

public boolean isStrong(String password) {
    int requirementsMet = 0;
    for(Pattern req : requirements) {
        if(req.matcher(password).matches())
            requirementsMet++;

        if(requirementsMet >= 3)
            return true;
    }

    return false;
}
Vince
  • 14,470
  • 7
  • 39
  • 84
1

I assume the four requirements, of which at three must be met, are as follows. The string must contain:

  1. a letter
  2. a digit
  3. a character in the string "!@#$%^&*"
  4. at least 8 characters

Is the use of a regular expression the best way to determine if a password meets three of the four requirements? That may be a valid question but it's not the one being asked or the one that I will attempt to answer. The OP may just be curious: can this problem be solved using a regular expression? Moreover, even if there are better ways to address the problem there is educational value in answers to the specific question that's been posed.

I am not familiar with Java, but I can suggest a regular expression that uses Ruby syntax. Readers unfamiliar with Ruby should be able to understand the expression, and its translation to Java should be straightforward. (If a reader can perform that translation, I would be grateful to see an edit to this answer that provides the Java equivalent at the end.)

r = /
    ((?=.*[a-z]))      # match a lowercase letter in the string in
                       # a positive lookahead in cap grp 1, 0 times
    ((?=.*\d))         # match a digit in the string in a positive
                       # lookahead in cap grp 2, 0 times
    ((?=.*[!@#$%^&*])) # match a special character in in the string
                       # in a positive lookahead in cap grp 3, 0 times
    (.{8,})            # match at least 8 characters in cap grp 4, 0 times
    \g<1>\g<2>\g<3>    # match conditions 1, 2 and 3
    |                  # or
    \g<1>\g<2>\g<4>    # match conditions 1, 2 and 4
    |                  # or
    \g<1>\g<3>\g<4>    # match conditions 1, 3 and 4
    |                  # or
    \g<2>\g<3>\g<4>    # match conditions 2, 3 and 4
    /xi                # case indiff & free-spacing regex def modes 

\g{2}, for example, is replaced by the sub-expression contained in capture group 2 ((?=.*\d)). The first four lines each contain an expression in a capture group, with the capture group repeated zero times. This is just a device to define the subexpressions in the capture groups for retrieval later.

Let's test some strings.

"Passw0rd".match? r  #=> true  (a letter, digit and >= 8)
"ab2c#45d".match? r  #=> true  (all 4 conditions satisfied)
"ab2c#5d".match?  r  #=> true  (a letter, digit and special char)
"ab2c345d".match? r  #=> true  (a letter, digit and >= 8)
"ab#c?def".match? r  #=> true  (a letter, special char and >= 8)
"21#6?512".match? r  #=> true  (a digit, special char and >= 8)

"ab26c4".match?   r  #=> false (only letter and digit)
"a$b#c".match?    r  #=> false (only letter and special char)
"abc ef h".match? r  #=> false (only letter and >= 8)
"12 45 78".match? r  #=> false (only digit and >=8)
"########".match? r  #=> false (only special char and >= 8)
"".match          r  #=> false (no condition matched) 

To use named capture groups, ((?=.*[a-z])) would be replaced with, say,

(?<letter>(?=.*[a-z]))

and \g<1>\g<2>\g<3> would be replaced by something like

\g<letter>\g<digit>\g<spec_char>
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

To answer your question, the sequence in which you have provided

1st: [a-zA-Z] characters

2nd: [0-9] Numbers

3rd: [!@#$%^&*] Sp. Chars

The occurrence of literals in this sequence is must. Abcd1234@ will pass but Abcd1234@A will not pass, as A appears again after [!@#$%^&*]

And a positive lookahead must include this sequence only. If you provied any special char before, it will not be validated, similarly in your case, characters after number is not expected.

use a positive lookahead for each combination, use groups for each

Try this instead(This is my work after several lookups):

^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*_])(?!.*[`~^=+/?<>():;-])(?=\S+$).{8,20}$

In this case: any of the provided chars or literals can appear anywhere.

(?=.*[0-9])

(?=.*[a-z])

(?=.*[A-Z])

(?=.*[!@#$%&*_])

(?!.*[`~^=+/?<>():;-])

(?=\S+$)
Kumar Ashutosh
  • 1,121
  • 10
  • 33
  • Also, there are some more errors in your reges, as it can match infine or zero. – Kumar Ashutosh Feb 28 '20 at 01:35
  • can you explain a little bit why "Passw0rd" didnt pass the conditions in regex? It has 3 conditions: uppercase, lower and digit – Trung Feb 28 '20 at 01:50
  • "Passw0rd" has "0" (zero) in between characters, but as per regex characyers can not be after numbers – Kumar Ashutosh Feb 28 '20 at 01:51
  • 1
    When your regex starts to look like a novel, it's probably not the right tool, or you are not using the tool with future maintenance in mind. Why not have 3 `matches` calls, using pre-compiled `Pattern` objects? It would look a lot cleaner at the cost of minimal performance overhead (if any overhead, considering he's currently not using a pre-compiled pattern). That way, you can remove the semantics of relativity (look-aheads and such), semantics which should not exist for a problem where relativity doesn't matter. – Vince Feb 28 '20 at 02:38