All Tools / Blog / How to Generate a Secure Random Password in JavaScript and Python

How to Generate a Secure Random Password in JavaScript and Python

5 min read

Most "random" password generators people write with Math.random() aren't actually secure. For a password that resists brute-force attacks, you need a cryptographically secure random number generator. This guide shows how to generate strong passwords correctly.

What makes a password secure?

Two factors determine password strength: length and character set size. Entropy (in bits) captures both:

entropy = log2(character_set_size ^ length)
         = length × log2(character_set_size)

Practical targets:

  • 128 bits — secure for most uses (equivalent to a random 128-bit key)
  • 80 bits — minimum for anything important
  • < 40 bits — crackable with consumer hardware
Password type Bits per character Length for 128-bit entropy
Lowercase only (26 chars) 4.7 28 characters
Mixed case + digits (62 chars) 5.95 22 characters
Full printable ASCII (94 chars) 6.55 20 characters
Diceware word (7,776 words) 12.9 10 words

A 20-character password using uppercase, lowercase, digits, and symbols has ~128 bits of entropy. That's the target.

JavaScript — browser

Use crypto.getRandomValues() — the only correct API for this. Never use Math.random().

function generatePassword(length = 20, options = {}) {
    const {
        uppercase = true,
        lowercase = true,
        digits = true,
        symbols = true,
    } = options;

    let chars = '';
    if (uppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    if (lowercase) chars += 'abcdefghijklmnopqrstuvwxyz';
    if (digits)    chars += '0123456789';
    if (symbols)   chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';

    if (!chars) throw new Error('At least one character set must be enabled');

    const array = new Uint32Array(length);
    crypto.getRandomValues(array);

    return Array.from(array, x => chars[x % chars.length]).join('');
}

console.log(generatePassword(20));
// "kT9!mP2#xQ7@rL5$nW8^"

Why the modulo bias issue doesn't matter much here: Uint32Array gives values 0–4,294,967,295. If your character set has 94 characters, the modulo isn't perfectly uniform — but the bias is tiny (less than 0.001%), which is irrelevant for password generation.

If you need perfect uniformity (e.g., for cryptographic key material), use rejection sampling:

function secureSample(chars, length) {
    const result = [];
    while (result.length < length) {
        const bytes = new Uint8Array(length * 2);
        crypto.getRandomValues(bytes);
        for (const b of bytes) {
            if (result.length >= length) break;
            if (b < Math.floor(256 / chars.length) * chars.length) {
                result.push(chars[b % chars.length]);
            }
        }
    }
    return result.join('');
}

JavaScript — Node.js

const crypto = require('crypto');

function generatePassword(length = 20) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';
    const bytes = crypto.randomBytes(length);
    return Array.from(bytes, b => chars[b % chars.length]).join('');
}

console.log(generatePassword(24));
// "Xm9kQ!rL2nP@jT5vW8sZ#hK"

Or using crypto.randomUUID() + trimming for a quick random string (not ideal — limited character set):

// Quick and dirty — uses only hex characters (0-9, a-f)
const id = crypto.randomUUID().replace(/-/g, '');

Python

Python's secrets module (Python 3.6+) is the correct tool — it uses the OS's cryptographically secure RNG.

import secrets
import string

def generate_password(length: int = 20) -> str:
    alphabet = string.ascii_letters + string.digits + string.punctuation
    return ''.join(secrets.choice(alphabet) for _ in range(length))

print(generate_password(20))
# "k!T9mP2#xQ7@rL5$nW8^"

secrets.choice() picks from the sequence using the OS CSPRNG. This is the right function. Don't use random.choice() — the random module is not cryptographically secure.

Custom character sets:

import secrets
import string

def generate_password(
    length: int = 20,
    uppercase: bool = True,
    lowercase: bool = True,
    digits: bool = True,
    symbols: bool = True,
) -> str:
    chars = ''
    required = []

    if uppercase:
        chars += string.ascii_uppercase
        required.append(secrets.choice(string.ascii_uppercase))
    if lowercase:
        chars += string.ascii_lowercase
        required.append(secrets.choice(string.ascii_lowercase))
    if digits:
        chars += string.digits
        required.append(secrets.choice(string.digits))
    if symbols:
        sym = '!@#$%^&*()-_=+[]{}|;:,.<>?'
        chars += sym
        required.append(secrets.choice(sym))

    if not chars:
        raise ValueError('At least one character set must be enabled')

    # Fill remaining length with random choices from full set
    remaining = length - len(required)
    pool = required + [secrets.choice(chars) for _ in range(remaining)]

    # Shuffle to avoid predictable positions (first char always uppercase, etc.)
    secrets.SystemRandom().shuffle(pool)
    return ''.join(pool)

print(generate_password(24))

This guarantees at least one character from each enabled set, then shuffles — so the first character isn't predictably uppercase.

Command line

macOS/Linux — openssl:

# 20-char base64 password
openssl rand -base64 20

# 20-char hex password
openssl rand -hex 20

# Alphanumeric-only (strip special characters)
openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 20

Linux — /dev/urandom:

# Printable ASCII characters
cat /dev/urandom | tr -dc 'a-zA-Z0-9!@#$%^&*' | head -c 20; echo

Python one-liner:

python3 -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters + string.digits + '!@#$%^&*') for _ in range(20)))"

Passphrases (Diceware)

A passphrase of 4–6 random words from a large wordlist can be more secure than a shorter random password — and much easier to remember.

import secrets

# EFF large wordlist has 7776 words (6 dice rolls)
# Download: https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt

def generate_passphrase(word_count: int = 5, wordlist_path: str = 'eff_large_wordlist.txt') -> str:
    with open(wordlist_path) as f:
        words = [line.split('\t')[1].strip() for line in f if '\t' in line]
    return '-'.join(secrets.choice(words) for _ in range(word_count))

print(generate_passphrase(5))
# "crumpet-waffle-harbor-slime-donkey"

5 words from the EFF wordlist gives ~64.6 bits of entropy. 6 words gives ~77.5 bits — equivalent to a 12-character random password with full ASCII.

What NOT to use

  • Math.random() (JavaScript) — uses a PRNG, not a CSPRNG. Predictable if seeded.
  • random.random() / random.choice() (Python) — same problem.
  • Any system that hashes a timestamp or username — not random.
  • Passwords under 12 characters — too short for anything sensitive.

Key takeaways

  • Use crypto.getRandomValues() in browser JavaScript, crypto.randomBytes() in Node.js.
  • Use secrets.choice() in Python — not random.choice().
  • Use openssl rand from the command line.
  • Target 20+ characters with mixed character sets for ~128 bits of entropy.
  • Passphrases (Diceware) are an excellent alternative — longer and more memorable.