Checking password strength without making users suffer

Checking password strength without making users suffer

"Minimum 8 characters, uppercase letter, digit, special character" — Sound familiar? Feels like filling out a government form. The password aaaaaA1! technically passes, while "LoremIpsum LoremIpsum LoremLorem IpsumIpsum" doesn't. So instead, I calculate entropy: it measures the actual difficulty of brute-forcing, not just ticking boxes for security theater. A long simple password can be stronger than a short "complex" one. Bonus — a funny error message that makes rejection a little less painful.

How to calculate entropy?

Easy — just count how many attempts are needed to brute-force the password! It's the number of possible characters (base) raised to the power of the length (exponent). That's it. Well, almost. Why? Because if the password is "aaaaaa", it formally has 6 characters, but in reality it's just one character repeated six times. So we need to account not only for length but also for character diversity. Let's get to it.

Step 1: Determine the base

Let's count which character classes are present in the password:

ClassAdds to base
Lowercase letters (a-z)+26
Uppercase letters (A-Z)+26
Digits (0-9)+10
Other characters (!@# etc.)+26

If you used lowercase, uppercase, and digits — base = 62. Only digits — base = 10.

Step 2: Exponent adjusted for repetitions

Each unique character counts as 1.0, each repeat — 0.3. What for? To reduce the weight of identical characters while still counting them — they do add some complexity, after all.

Exponent = unique_count × 1.0 + repeat_count × 0.3

Step 3: Threshold

We set a threshold — a balance between user convenience and security. I chose 10¹², which is roughly the level where brute-force becomes impractical for online attacks. If the result (base raised to the exponent) is below the threshold — the password is weak, and the user has to come up with a better one.

Shortest passwords that still pass (remove one character and they fail)

Password Base Uniq.Rep.Exponent Result
V1@dDra 88 7 0 7 88^7 ≈ 40e12
V1@dV1@dV1@d 88 4 8 6.4 88^6.4 ≈ 3e12
VladVladVladVl 52 4 10 7 52^7 ≈ 1e12
vladvladvladvladvla264 15 8.5 26^8.5 ≈ 1e12
123456789012345671010 7 12.1 10^12.1 ≈ 1e12
1111111111111111111111111111111111111111013812.410^12.4 ≈ 2e12
horsejump26 9 0 9 26^9 ≈ 5e12

As you can see, the very first password with just 7 characters is the hardest to crack. And the second one — 12 characters — would sail through complexity checks on most websites, yet in my system it barely scrapes by.

And a funny error message to get the user to actually pick a stronger password

Error: this password uses Epstein, choose another one

Code example

var funnyNames = []string{
	"Alice", "Bob", "Gizmo", "Noodle", "Pickle", "Wombat", "Bubbles", "Sprocket", "Muffin",
	"Einstein", "Tesla", "Curie", "Newton", "Turing", "Lovelace", "Hawking",
	"Gandalf", "Yoda", "Sherlock", "Pikachu", "Groot", "Dobby", "Epstein",
}

func checkPasswordStrength(password string) error {
	const minComplexity = 1e12
	var hasLower, hasUpper, hasDigit, hasOther bool
	unique := make(map[rune]struct{})
	for _, r := range password {
		switch {
		case unicode.IsLower(r):
			hasLower = true
		case unicode.IsUpper(r):
			hasUpper = true
		case unicode.IsDigit(r):
			hasDigit = true
		default:
			hasOther = true
		}
		unique[r] = struct{}{}
	}

	baseComplexity := 0
	if hasLower {
		baseComplexity += 26
	}
	if hasUpper {
		baseComplexity += 26
	}
	if hasDigit {
		baseComplexity += 10
	}
	if hasOther {
		baseComplexity += 26
	}

	total := utf8.RuneCountInString(password)
	exponent := float64(len(unique)) + float64(total-len(unique))*0.3
	result := math.Pow(float64(baseComplexity), exponent)
	if result < minComplexity {
		name := funnyNames[rand.Intn(len(funnyNames))]
		return fmt.Errorf("error, this password uses %s, choose another one", name)
	}
	return nil
}

Try it yourself with this interactive demo

Entered passwords are not sent anywhere. Trust me.

P.S. Use a password manager. You only need to remember one password. The rest can be long, complex, and always unique.

Comments