Password Security and Entropy: Why Length Beats Complexity

26 February, 2026 Security

Complexity requirements have been the default password policy for decades: uppercase, lowercase, number, symbol, and call it done. Then NIST published SP 800-63B in 2017 and quietly told everyone they had been wrong. The research shows that P@ssw0rd1 is not meaningfully more secure than password1 against a prepared attacker - and that length is the metric that actually matters. This article works through the math so you can understand exactly why, and how to implement genuinely secure password generation in your applications.

What Is Password Entropy

Entropy in the context of passwords means unpredictability - specifically, how many bits of information a password contains when chosen from a given method. A password with H bits of entropy requires an attacker to try up to 2^H guesses in the worst case to find it by brute force.

The concept comes from information theory (Shannon entropy), but for password analysis we use a simplified model: we care about the size of the search space, not the compression complexity of a specific string. The two are related but distinct. When a policy forces P@ssword1 as a pattern, the actual entropy is far lower than the theoretical maximum, because real-world attackers use that pattern in their wordlists.

Entropy gives you a precise, comparable measurement across different password schemes. Instead of arguing whether symbols are "better" than extra characters, you calculate bits and compare directly.


How Entropy Is Calculated

The formula is:

H = L x log2(N)

Where:

  • H = entropy in bits
  • L = length of the password (number of characters)
  • N = size of the character set (number of possible characters at each position)

Each character position contributes log2(N) bits. A password's total entropy is the sum of those contributions across all positions.

Concrete Examples

Lowercase letters only (a-z), 8 characters:

  • N = 26, L = 8
  • H = 8 x log2(26) = 8 x 4.7 = 37.6 bits

Mixed case + digits (a-z, A-Z, 0-9), 8 characters:

  • N = 62, L = 8
  • H = 8 x log2(62) = 8 x 5.95 = 47.6 bits

Full printable ASCII (95 characters), 8 characters:

  • N = 95, L = 8
  • H = 8 x log2(95) = 8 x 6.57 = 52.5 bits

Full printable ASCII, 12 characters:

  • N = 95, L = 12
  • H = 12 x log2(95) = 12 x 6.57 = 78.8 bits

The jump from 8 to 12 characters adds 26 bits. Adding a symbol set (going from 62 to 95 characters) only adds about 5 bits per character - and you only get that benefit if the symbols are chosen randomly, not substituted predictably (@ for a, 0 for o).

The formula assumes each character is chosen uniformly at random and independently. The moment you apply rules - capitalize the first letter, end with a digit, substitute symbols - you reduce the effective N without changing the apparent character set.


Length vs Complexity

The myth that complexity beats length comes from a misreading of how attacks work. An attacker with a modern GPU cluster does not try every possible character combination in order. They work through:

  1. Common wordlists (RockYou, Have I Been Pwned dumps)
  2. Rule-based mutations on those words (password to P@ssw0rd, password1, PASSWORD)
  3. Markov-chain generated candidates based on real password patterns
  4. Only then, brute force from the full character set

Against strategies 1-3, P@ssw0rd1 offers nearly no resistance - it appears directly in mutation rulesets. Hashcat ships with rules that generate it automatically. Against strategy 4, the length of a truly random password determines how long brute force takes.

The Math on "Secure Complexity"

P@ssword1 contains 9 characters from a 95-character set. Theoretical maximum: 9 x 6.57 = 59 bits. Actual entropy against a wordlist + rules attack: effectively 0, because it is in the wordlist.

A randomly generated 12-character lowercase password (xvqmjptkrfan) has 56.4 bits of theoretical entropy and no wordlist match. It is more resistant to practical attack despite using a smaller character set.

NIST SP 800-63B section 5.1.1 now recommends:

  • Minimum 8 characters for memorized secrets
  • Maximum length of at least 64 characters
  • No mandatory complexity rules
  • Screening against known compromised password lists
  • No periodic forced resets (they cause predictable patterns like Password2025!)

Character Sets and Their Bits

The per-character entropy contribution for each common charset:

Character Set Size (N) Bits per Character
Digits only (0-9) 10 3.32
Lowercase (a-z) 26 4.70
Lowercase + digits 36 5.17
Mixed case (a-zA-Z) 52 5.70
Mixed case + digits 62 5.95
Mixed case + digits + 8 symbols 70 6.13
Full printable ASCII 95 6.57
ASCII + extended (Latin-1) 191 7.58

Note the diminishing returns: going from lowercase-only (4.70 bits) to full printable ASCII (6.57 bits) gains less than 2 bits per character. Adding 4 characters to a lowercase-only password (4 x 4.70 = 18.8 bits) beats that gain by an order of magnitude.


Passphrases vs Random Strings

The Diceware Method

Diceware generates passphrases by rolling physical dice and looking up results in a 7776-word list (6^5 words). Each word contributes log2(7776) = 12.92 bits of entropy.

A 4-word Diceware passphrase: 4 x 12.92 = 51.7 bits A 6-word Diceware passphrase: 6 x 12.92 = 77.5 bits A 7-word Diceware passphrase: 7 x 12.92 = 90.5 bits

The EFF wordlist (2016) is designed so every word is memorable and distinct. Six words produce a passphrase that exceeds the entropy of a random 12-character full-ASCII password.

XKCD 936

The webcomic "correct horse battery staple" (XKCD #936, 2011) illustrated this principle with a specific claim: four random common English words have around 44 bits of entropy (using a 2048-word common vocabulary: log2(2048) = 11 bits x 4 = 44 bits). That is weaker than Diceware because the vocabulary is smaller and more guessable. The comic's point was directionally correct - the specific numbers vary significantly by wordlist.

Comparison Table

Scheme Charset / Source Length Entropy (bits)
Common password (P@ssword1) 95 chars (theoretical) 9 ~0 effective
8-char random lowercase 26 chars 8 37.6
8-char random full ASCII 95 chars 8 52.5
12-char random full ASCII 95 chars 12 78.8
16-char random full ASCII 95 chars 16 105.1
4-word Diceware 7776 words 4 words 51.7
6-word Diceware 7776 words 6 words 77.5
6-word common words 2048 words 6 words 66.0

Passphrases trade memorability for modest length overhead. For machine-generated secrets (API keys, database passwords), a random 20+ character string from a full charset is the unambiguous right choice.


Common Attacks: Brute Force

Brute force iterates through every possible combination. Its feasibility depends on:

  • Attack speed (hashes checked per second)
  • Password entropy (search space size)
  • Hashing algorithm used to store the password

A consumer NVIDIA RTX 4090 benchmarks at approximately:

  • 164 billion MD5 hashes per second
  • 21 billion bcrypt (cost 5) hashes per second
  • 184 million bcrypt (cost 12) hashes per second
  • 1.4 million Argon2id hashes per second (reference parameters)

At 184 million bcrypt/s against a 52-bit password (8 chars, full ASCII):

  • Search space: 2^52 = 4.5 x 10^15
  • Time to exhaust: 4.5 x 10^15 / 184 x 10^6 = roughly 280 days on a single GPU

Against a 78-bit password (12 chars, full ASCII), the same hardware would require approximately 2^78 / 184 x 10^6 hashes - on the order of tens of billions of years. Length makes brute force computationally infeasible long before "impossibly complex" character sets do.


Common Attacks: Dictionary and Rainbow Tables

Dictionary Attacks

Modern dictionary attacks use mutated wordlists. The RockYou leak (2009, ~14 million passwords) and subsequent breaches have produced wordlists containing hundreds of millions of real passwords. Hashcat's best64.rule ruleset applies 64 common transformations (capitalize, append digits, substitute characters) to each base word, multiplying the effective dictionary size.

Against a password that follows any recognizable pattern - even one that satisfies complexity requirements - dictionary + rules attacks succeed in seconds to hours.

Rainbow Tables

A rainbow table is a precomputed lookup structure: it maps password hashes back to the original passwords without storing every hash individually. A 2007 rainbow table for MD5-hashed 8-character alphanumeric passwords fits in under 1 GB and can crack hashes in seconds (time-memory trade-off, Hellman chains).

Salting defeats rainbow tables completely. A cryptographic salt is a random value, unique per user, prepended or appended to the password before hashing. Since each hash now includes a unique random component, precomputed tables are useless - an attacker must attack each hash individually. bcrypt, Argon2, and scrypt all incorporate salting automatically.

Never use unsalted hashing (plain MD5, SHA-1, SHA-256) for passwords. CVE-2012-3488 and dozens of similar CVEs document real systems that stored unsalted hashes.

The correct algorithm hierarchy for password storage in 2025:

  1. Argon2id - OWASP recommended, PHC winner, memory-hard
  2. bcrypt - proven, widely supported, cost factor adjustable
  3. scrypt - memory-hard, acceptable alternative

Do not use PBKDF2 for new systems unless FIPS compliance is required.


Password Managers

The fundamental problem with password security advice is human memory. A person who uses a unique, randomly generated 20-character password for every service is meaningfully secure. A person who reuses a memorable but weak password everywhere is not.

Password managers resolve this by:

  • Generating cryptographically random passwords per site
  • Storing them encrypted (typically AES-256-GCM or XChaCha20-Poly1305) behind one master password
  • Autofilling so users never type stored passwords (defeats keyloggers for most scenarios)

A well-implemented password manager (Bitwarden, 1Password, KeePass) reduces the practical problem to protecting one strong master passphrase. That master passphrase should be a long Diceware passphrase - memorable by design, high entropy by construction.

For developers: consider whether your application should support passkeys (WebAuthn / FIDO2) instead of passwords. NIST SP 800-63B-4 (2024 draft) formally recommends phishing-resistant authenticators as the preferred approach.


Generating Secure Passwords

The critical requirement for secure password generation is a cryptographically secure random number generator (CSPRNG). Standard library rand() functions in most languages are not CSPRNGs - they are seeded with predictable values and produce statistically predictable output.

/generators/password

PHP

<?php
declare(strict_types=1);

function generatePassword(int $length = 16, string $charset = ''): string
{
    if ($charset === '') {
        $charset = 'abcdefghijklmnopqrstuvwxyz'
            . 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
            . '0123456789'
            . '!@#$%^&*()-_=+[]{}|;:,.<>?';
    }

    $charsetLength = strlen($charset);
    $password = '';

    for ($i = 0; $i < $length; $i++) {
        // random_int() uses the OS CSPRNG (getrandom() on Linux)
        $password .= $charset[random_int(0, $charsetLength - 1)];
    }

    return $password;
}

// 16-char full charset: ~105 bits of entropy
echo generatePassword(16);

// Passphrase from a wordlist
function generatePassphrase(array $wordlist, int $wordCount = 6): string
{
    $words = array_map(
        static fn (): string => $wordlist[random_int(0, count($wordlist) - 1)],
        range(1, $wordCount)
    );

    return implode('-', $words);
}

random_int() was added in PHP 7.0 and wraps the OS CSPRNG. Never use rand() or mt_rand() for security-sensitive values.

Python

import secrets
import string

def generate_password(length: int = 16, charset: str = '') -> str:
    if not charset:
        charset = (
            string.ascii_lowercase
            + string.ascii_uppercase
            + string.digits
            + '!@#$%^&*()-_=+[]{}|;:,.<>?'
        )
    # secrets.choice() uses os.urandom() internally
    return ''.join(secrets.choice(charset) for _ in range(length))

def generate_passphrase(wordlist: list[str], word_count: int = 6) -> str:
    return '-'.join(secrets.choice(wordlist) for _ in range(word_count))

# 20-char password: ~131 bits of entropy
print(generate_password(20))

The secrets module (Python 3.6+) is the correct choice. Never use random.choice() for passwords - random is seeded deterministically and is not cryptographically secure.

JavaScript

// Node.js
import { randomInt } from 'node:crypto';

function generatePassword(length = 16, charset = '') {
    if (!charset) {
        charset =
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +
            '0123456789!@#$%^&*()-_=+[]{}|;:,.<>?';
    }

    let password = '';
    for (let i = 0; i < length; i++) {
        password += charset[randomInt(0, charset.length)];
    }
    return password;
}

// Browser - Web Crypto API (all modern browsers)
function generatePasswordBrowser(length = 16) {
    const charset =
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +
        '0123456789!@#$%^&*()-_=+[]{}|;:,.<>?';
    const array = new Uint32Array(length);
    crypto.getRandomValues(array);
    return Array.from(array, (n) => charset[n % charset.length]).join('');
}

Math.random() is explicitly not a CSPRNG. Use crypto.getRandomValues() in browsers and node:crypto in Node.js.


Conclusion

Password entropy is not an abstract concept - it is a measurable property with direct consequences for how long a password resists attack. The formula H = L x log2(N) gives you a precise tool to compare schemes and make informed decisions.

The key findings from the math:

  • Length multiplies entropy linearly. Every extra character at full ASCII adds 6.57 bits.
  • Character set growth is logarithmic and saturates quickly. Going from 62 to 95 characters adds less than 0.7 bits per character.
  • Complexity rules that enforce recognizable patterns reduce real entropy dramatically, regardless of what the theoretical charset implies.
  • Salting with Argon2id, bcrypt, or scrypt is non-negotiable for password storage.
  • CSPRNGs (random_int(), secrets, crypto.getRandomValues()) are mandatory for password generation.

Generate a strong, properly random password right here: /generators/password

More Articles

UUID Versions Explained: v1, v3, v4, v5, v6, and v7

A complete technical breakdown of all UUID versions. Covers time-based, name-based, and random UUIDs, with code examples in PHP, Python, and JavaScript, and a practical guide to choosing the right version.

28 February, 2026

RAG Document Assistant: Answer Questions from Your Own Docs with Ollama, ChromaDB and Docker

Build a local RAG document assistant that reads .txt files, indexes them with vector embeddings, and answers questions using a local LLM — all without a cloud API. Includes a FastAPI backend, a minimal browser UI, and a full Docker Compose setup.

26 February, 2026

Free Local LLM in Docker: Build a Customer Feedback Analyser with Ollama and Pydantic

How to run Ollama in Docker Compose, pull a model on first start, and build a Python CLI that reads customer reviews from CSV, clusters them by theme, and generates a structured report — using Pydantic schemas and system/user message separation. No API keys, no monthly bills.

25 February, 2026