Password requirements are harmful. A password with required digit/special character may be weaker.

This is counterintuitive, but such (popular) password requirements may weaken your password, like:

A password is said to be strong if it satisfies the following criteria: 

    It contains at least 8 characters.
    It contains at least one digit.
    It contains at least one lower case alphabet.
    It contains at least one upper case alphabet.
    It contains at least one special character which includes !@#$%^&*()-+

( src )

Password length: Minimum of 8 Characters 

Password complexity: Passwords must contain
At least one upper case A-Z 
At least one lowercase letter (a-z)
At least one digit 0-9 
At least one punctuation character, such as, !@#$%^&*()-_+|~-=\`{}[]:";'[]?,./ 

( src )

Let's see. This code enumerates all passwords with a-z0-9 chars:

#!/usr/bin/env python3
alphabet="qwertyuiopasdfghjklzxcvbnm0123456789"

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                print (a+b+c+d)

And this is the same, but a digit must be present:

#!/usr/bin/env python3
alphabet="qwertyuiopasdfghjklzxcvbnm0123456789"

def is_digit(c):
    return c in "0123456789"

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                if is_digit(a) or is_digit(b) or is_digit(c) or is_digit(d):
                    print (a+b+c+d)

There are slightly more passwords in first case:

 % ./v1_1.py | wc -l
1679616

 % ./v1_2.py | wc -l
1222640

Now let's say, the alphabet consisting of a-z0-9 and punctuation symbols:

#!/usr/bin/env python3
punct=" !\"#$%&'()*+,-./:;<=>?"
#        ^ note the escape char before double quotes

alphabet="qwertyuiopasdfghjklzxcvbnm0123456789"+punct

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                print (a+b+c+d)

The second version (a digit must be present in password):

#!/usr/bin/env python3
punct=" !\"#$%&'()*+,-./:;<=>?"
#        ^ note the escape char before double quotes

alphabet="qwertyuiopasdfghjklzxcvbnm0123456789"+punct

def is_digit(c):
    return c in "0123456789"

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                if is_digit(a) or is_digit(b) or is_digit(c) or is_digit(d):
                    print (a+b+c+d)

The second version almost halved the results of the first version:

 % ./v2_1.py | wc -l
11316496
 % ./v2_2.py | wc -l
6008080

Now the first version again:

#!/usr/bin/env python3
punct=" !\"#$%&'()*+,-./:;<=>?"
#        ^ note the escape char before double quotes

alphabet="qwertyuiopasdfghjklzxcvbnm"+"0123456789"+punct

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                print (a+b+c+d)

The second version, a digit OR punctuation character must be present in password:

#!/usr/bin/env python3
punct=" !\"#$%&'()*+,-./:;<=>?"
#        ^ note the escape char before double quotes

alphabet="qwertyuiopasdfghjklzxcvbnm"+"0123456789"+punct

def is_special(c):
    return c in "0123456789"+punct

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                if is_special(a) or is_special(b) or is_special(c) or is_special(d):
                    print (a+b+c+d)

Third version: at least one punctuation character must be present AND and least one digit:

#!/usr/bin/env python3
punct=" !\"#$%&'()*+,-./:;<=>?"
#        ^ note the escape char before double quotes

alphabet="qwertyuiopasdfghjklzxcvbnm"+"0123456789"+punct

def is_digit(c):
    return c in "0123456789"

def is_punct(c):
    return c in punct

for a in alphabet:
    for b in alphabet:
        for c in alphabet:
            for d in alphabet:
                has_at_least_one_punct=is_punct(a) or is_punct(b) or is_punct(c) or is_punct(d)
                has_at_least_one_digit=is_digit(a) or is_digit(b) or is_digit(c) or is_digit(d)
                if has_at_least_one_punct and has_at_least_one_digit:
                    print (a+b+c+d)

The third version halved the results of first and second:

 % ./v3_1.py | wc -l
11316496
 % ./v3_2.py | wc -l
10859520
 % ./v3_3.py | wc -l
4785440

I simplified Python code for demonstration, but itertools module is to be used instead of nested loops, of course.

TL;DR: --- a randomly generated password (using your favorite password manager or a simple script) is stronger.

In plain English --- knowing password requirements, attacker have more information about password.

Be very careful when devising password requirements.


Now let's compare 4-character password of a-z lowercase character and 3-character a-z password with a random digit inserted somewhere between a-z characters.

Just password: 4-character a-z password: 26**4=456976. 8-character a-z password: 26**8=208827064576.

A digit can be inserted between characters, at the beginning of the password, or at the end, that is, (len+1)*10. 4-character password with 3 a-z characters and one digit somewhere: 26**3*(5*10)=878800 (~2x search space increase). 8-character password with 7 a-z characters and one digit somewhere: 26**7*(9*10)=722862915840 (~3.5x search space increase).

Yes, this is better.

But of course, most users, if coerced into using a digit, would add a random digit at the end of their password (like 'password3'), instead of inserting it somewhere in the middle (like 'pas3sword').

And what is better? Password of n+1 a-z characters? Or n a-z passwords plus one digit?

Keep in mind: adding a random a-z lowercase character at the end multiples search space by 26. But adding a random digit multiples it only by 10. A 8-character a-z password has larger search space than a 7-character a-z password with a random digit added, by factor of ~2.5x.

What does all these 'factors' mean in practice? It mean increase/decrease the time a cracker/hacker/attacker would need to try all passwords (brute-forcing).


All such problems/tasks can be found in many combinatorics textbooks.

(the post first published at 20250327.)


List of my other blog posts.

Subscribe to my news feed,

Some time ago (before 24-Mar-2025) there was Disqus JS script for comments. I dropped it --- it was so motley, distracting, animated, with too much ads. I never liked it. Also, comments didn't appeared correctly (Disqus was buggy). Also, my blog is too chamberlike --- not many people write comments here. So I decided to switch to the model I once had at least in 2020 --- send me your comments by email to blog at yurichev dot com (don't forget to include URL to this blog post) and I'll copy&paste it here manually

Let's party like it's ~1993-1996, in this ultimate, radical and uncompromisingly primitive pre-web1.0-style blog and website.