ProXicT
ProXicT

Reputation: 1933

Regex specific password restrictions

I need to validate password to fulfill at least three combinations of these rules:
Upper case, lower case, number, special char
The password also needs to be at least 8 characters long.

EDIT
I know these password restrictions are contraproductive, it's a request, not my naive idea of getting stronger passwords.

Example of valid passwords:

Password3   // Upper case, lower case, number
Password!   // Upper case, lower case, special
password3!  // Lower case, number, special
PASSWORD3!  // Upper case, number, special
Password3!  // Upper case, lower case, number, special

Example of invalid passwords:

password    // Only lower case
password3   // Only lower case and number
password!   // Only lower case and special
Password    // Only Upper case and lower case
Pw3!        // Too short

Here is a regex I wrote:

(?=.*[A-z\d]+)(?=.*[A-z@!?]+)(?=.*[a-z\d@!?]+)(?=.*[A-Z\d@!?]+)[A-z\d@!?]{8,}

Hopefuly, I know why this regex matches invalid passwords too, if so, how can I say then, that the expression must contain everything from the group and not just one match?

Upvotes: 0

Views: 3179

Answers (2)

Schwern
Schwern

Reputation: 165396

Let's get this out of the way, password strength checkers like this do not work.

In theory, password strength checkers do not work. That's because the strength of a password does not depend upon the password value (which you give to the checker) but upon the password generation process (which you do not formalize often, let alone enter in the checker).

Here's Sophos on the subject: "Why you can’t trust password strength meters" and "Why you STILL can’t trust password strength meters".

Simple password meters check the length and entropy of the password and have checklists for the kinds of things that users are advised to include in their passwords; mixtures of upper and lower case letters, numbers and special characters, for example.

That helps determine a password’s ability to withstand a brute force attack (an attacker making guesses at random), but being resistant to brute force attacks is only useful if that’s what an attacker is going to do, and it probably isn’t.

Here's Concordia University, "From Very Weak to Very Strong: Analyzing Password-Strength Meters".

In our large-scale empirical analysis, it is evident that the commonly-used meters are highly inconsistent, fail to provide coherent feedback on user choices, and sometimes provide strength measurements that are blatantly misleading.

All the breeches of password databases over the years have given attackers years to study hundreds of millions of real passwords and look for exploitable patterns. P4ssw0rd! isn't going to work anymore, they're on to our tricks. They know about replacing a with 4. They know about sticking a punctuation character at the end. They know about upper casing the first letter of a dictionary word. Attackers use low cost GPUs as little super computers to do extreme parallel processing to try all of these tricks.

More important is to measure password entropy, basically how hard it is to match your password with an existing pattern. How random looking is it? Humans are really bad at creating entropy. Writing an entropy checker is beyond the scope of this answer.

The only way to make a secure password is to randomly generate a new one for each account using the maximum available length, and storing it in a password vault. Any system which involves making up passwords in your head and remembering them has already been broken.

Point is: don't bother. You're closing the door LONG after the horse has left the barn.

Now onto the question.


Do it in multiple regexes

You could do it as one regex, but then you'd have an unreadable, unmaintainable mess. If you ever wanted to add to it, good luck.

The other reason is it's difficult to figure out from one check why the password failed. Nothing is more annoying than being told your password is invalid but not why. Be sure to design your password checking function so it says which check failed.

The basic pattern is to write a series of checks for it to fail. If it passes all those, it passes. Here's a sketch in Perl.

sub is_invalid_password {
    my $password = shift;

    # Instead of hard coding the wording of the message,
    # use constants. This lets the UX person handle the
    # wording. Also helps internationalization.
    return NEEDS_UPPER_CASE unless $password =~ /[A-Z]/;
    return NEEDS_LOWER_CASE unless $password =~ /[a-z]/;

    ...and so on...

    return 0;
}

It's checking for an invalid password so it can return the error constant on failure. This makes it much simpler to use. Just make sure all the constants are true values and you can write:

if( my $reason = is_invalid_password($password) ) {
    # Don't just say the password is invalid, say why.
    display_invalid_password_reason($reason);
}
else {
    store_password($password);
}

The regexes are...

  1. /[A-Z]/
  2. /[a-z]/
  3. /[0-9]/
  4. /[^A-Za-z0-9]/

Oh, wait... Unicode. You have to use POSIX character classes else you'll reject Élan1;. These are how Perl does it, your language may vary.

  1. /[[:upper:]]/
  2. /[[:lower:]]/
  3. /[[:digit:]]/
  4. /[^[:alnum:]]/

It's very easy to do this wrong and annoy the user. It's a bit of work to do it right. It's not adding any security. This is why I recommend don't bother.

Upvotes: 1

Stack Danny
Stack Danny

Reputation: 8156

It's obvious that your problem is a kinda hard one. So I would split it into more understandable pieces. That will help so much. Look at this example I made:

#include <iostream>
#include <string>
#include <regex>

int main(){

    bool upper_case = false; //saves the result if upper-case characters were found.
    bool lower_case = false; //same for lower-case
    bool number_case = false; //...
    bool special_char = false;


    std::regex upper_case_expression{ "[A-Z]+" }; //here is the very simple expression for upper_case search
    std::regex lower_case_expression{ "[a-z]+" }; //for lower-case
    std::regex number_expression{ "[0-9]+" }; //...
    std::regex special_char_expression{ "[@!?]+"};


    std::string pw;


    bool done = false; //let's assume we're not done

    do{ //do ask-for-password as long were not done

        std::cout << "Type in a valid password: ";
        std::getline(std::cin, pw); //get input

        if (pw.length() <= 8){ //too short!
            std::cout << "Invalid password! Try again . . .\n\n";
        }
        else{

            upper_case = std::regex_search(pw, upper_case_expression); //save the result, if the expression was found.
            lower_case = std::regex_search(pw, lower_case_expression); //....
            number_case = std::regex_search(pw, number_expression);
            special_char = std::regex_search(pw, special_char_expression);

            //like: sum_of_positive_results = 1 + 0 + 1 + 1 (true/false as an integer)
            int sum_of_positive_results = upper_case + lower_case + number_case + special_char; 

            if (sum_of_positive_results < 3){ //not enough booleans were true!
                std::cout << "Invalid password! Try again . . .\n\n";
            }
            else{ //otherwise it's valid!
                std::cout << "That's a valid Password!" << std::endl;
                done = true;
            }
        }

    } while (!done);

    return 0;
}

output:

Type in a valid password: password
Invalid password! Try again . . .

Type in a valid password: Password
Invalid password! Try again . . .

Type in a valid password: password!
Invalid password! Try again . . .

Type in a valid password: password3
Invalid password! Try again . . .

Type in a valid password: not valid!
Invalid password! Try again . . .

Type in a valid password: Password!
That's a valid Password!

As you can see, that's way nicer and easier to follow. I hope that helped you!

Upvotes: 1

Related Questions