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:
| Class | Adds 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 |
| vladvladvladvladvla | 26 | 4 | 15 | 8.5 | 26^8.5 ≈ 1e12 |
| 12345678901234567 | 10 | 10 | 7 | 12.1 | 10^12.1 ≈ 1e12 |
| 111111111111111111111111111111111111111 | 10 | 1 | 38 | 12.4 | 10^12.4 ≈ 2e12 |
| horsejump | 26 | 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
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