12-character password generator

Twelve characters is the PCI DSS minimum and three short of the NIST one. This page explains that gap precisely.

G
More options
up to 10,000

Constraints

Where the number 12 comes from

Twelve is not a round number chosen for aesthetics. It is the minimum in PCI DSS v4.0.1 requirement 8.3.6, which has been in force since 31 March 2025 and applies to accounts in the cardholder data environment. The requirement is 12 characters containing both numeric and alphabetic characters, dropping to 8 where a system genuinely cannot support 12, and it retains 90-day rotation for accounts not protected by multi-factor authentication.

NIST SP 800-63B-4 sets a different bar. Its single-factor minimum is 15 characters, stated as a SHALL, and it explicitly forbids composition requirements: "Other composition requirements for passwords SHALL NOT be imposed." So the two standards disagree about the length and disagree about whether the class rule should exist at all. If a security policy cites both without noticing, it has a contradiction in it.

The practical resolution is that 12 is a floor, not a target. Generate at 15 or more and you satisfy NIST outright while comfortably clearing the PCI minimum, and the composition argument becomes moot because a mixed-alphabet draw of that length will almost always contain everything anyone asks for. Twelve is the right choice when a field will not take more, or when an auditor is checking against PCI specifically.

74.5 bits, and where that sits

Twelve characters from the 74-character default alphabet gives 74.51 bits. With all four classes required, the honest figure is 74.02 bits, because the constraint removes every string that misses a class. That is above 64 bits, which is the line NIST SP 800-63B-4 uses in two places: below it a verifier SHALL rate limit, and at or above it a secret is acceptable for recovery codes and for uses that cannot rely on rate limiting.

Against measured cracking rates on one RTX 5090, 74.02 bits is about 1,400 years behind MD5 and roughly a billion years behind bcrypt at cost 5. The number that keeps it honest is the large-cluster case: at a hundred thousand GPUs running MD5, the expected time falls to about five days. That is not a reason to panic, but it is the reason the recommendation keeps climbing, and it is why 15 or 16 characters is a better place to sit if the field allows.

Twelve characters, by alphabet
AlphabetNTotal at length 12
Lowercase only2656.41 bits
Alphanumeric6271.45 bits
Alphanumeric plus 12 safe symbols7474.51 bits
Printable ASCII except space9478.66 bits

Satisfying a class rule without leaving it to chance

A uniform draw is not guaranteed to contain one of everything, and at this length the shortfall is common rather than rare. Of uniform 12-character draws from the 74-character alphabet, 29.0% are missing at least one of the four classes. A generator that simply produces a string and hands it over will therefore fail a strict four-class check roughly three times in ten, which is why so many sites bounce a perfectly good generated password back at you.

The narrower PCI check, numeric and alphabetic only, fails less often but still fails. For the widely used case of a 16-character alphanumeric password, the probability of containing no digit at all is (52/62) to the 16th power, which is exactly 0.05995, or 6.0%. One in seventeen.

This tool enforces the constraint instead of hoping. It generates a complete candidate, checks it against every required class, and discards the whole string on failure rather than patching it. Patching is what most implementations do, and it introduces a positional pattern: if position one is always uppercase and position two always a digit, the output is no longer uniform and the stated entropy is an overstatement. Whole-string rejection stays uniform over the valid set, so the 74.02 bit figure is exact.

The expected number of attempts at length 12 with four required classes is 1.41. The cost of correctness here is a fraction of a millisecond, which is why the shortcut has no excuse.

Doing this in your own code

JavaScript
// 74-character alphabet: A-Z a-z 0-9 and the 12 safe symbols.
const ALPHABET =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

// Rejection sampling. v % n on a raw uint32 is biased whenever n does not
// divide 2^32, and 74 does not.
function pick(n) {
  const limit = Math.floor(2 ** 32 / n) * n;
  const buf = new Uint32Array(1);
  let v;
  do {
    crypto.getRandomValues(buf);
    v = buf[0];
  } while (v >= limit);
  return v % n;
}

function password(length = 20) {
  let out = "";
  for (let i = 0; i < length; i++) out += ALPHABET[pick(ALPHABET.length)];
  return out;
}

Math.random() is never acceptable here. V8 implements it with xorshift128+, whose internal state is recoverable from a handful of outputs.

Python 3.6+
import secrets
import string

ALPHABET = string.ascii_letters + string.digits + "!#$%*+-=?@^_"

def password(length: int = 20) -> str:
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

# With a required-class check, resample the WHOLE string on failure.
def password_all_classes(length: int = 20) -> str:
    while True:
        p = password(length)
        if (any(c.isupper() for c in p) and any(c.islower() for c in p)
                and any(c.isdigit() for c in p)
                and any(c in "!#$%*+-=?@^_" for c in p)):
            return p

secrets.choice draws through randbelow, which rejection samples. random.choice rejection samples too, but it runs on the Mersenne Twister and must not be used for a password.

Go
import (
    "crypto/rand"
    "math/big"
)

const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_"

func Password(n int) (string, error) {
    b := make([]byte, n)
    max := big.NewInt(int64(len(alphabet)))
    for i := range b {
        v, err := rand.Int(rand.Reader, max)
        if err != nil {
            return "", err
        }
        b[i] = alphabet[v.Int64()]
    }
    return string(b), nil
}

crypto/rand.Int rejection samples internally. math/rand is seeded and predictable, and importing the wrong one is a common review miss.

PHP 7+
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_';
$n = strlen($alphabet);

$password = '';
for ($i = 0; $i < 20; $i++) {
    $password .= $alphabet[random_int(0, $n - 1)];
}

random_int throws rather than falling back to a weak source. rand() and mt_rand() are not suitable for credentials.

Java
import java.security.SecureRandom;

static final String ALPHABET =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

static String password(int length) {
    SecureRandom rnd = new SecureRandom();
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
        sb.append(ALPHABET.charAt(rnd.nextInt(ALPHABET.length())));
    }
    return sb.toString();
}

nextInt(bound) rejection samples. Do not build one from nextDouble or from nextInt() % n.

C# / .NET 8+
using System.Security.Cryptography;

const string Alphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

string Password(int length) =>
    new string(RandomNumberGenerator.GetItems<char>(Alphabet, length));

RandomNumberGenerator.GetItems arrived in .NET 8 and is unbiased. On older targets, loop over GetInt32(0, Alphabet.Length).

Shell
# tr discards every byte outside the set, which is rejection sampling by
# construction. The hyphen must stay last inside the bracket.
LC_ALL=C tr -dc 'A-Za-z0-9!#$%*+=?@^_-' < /dev/urandom | head -c 20; echo

# A 192-bit token, if you do not need a typeable password. 24 bytes is
# 32 base64 characters with no padding, in the standard +/ alphabet.
openssl rand -base64 24

Without LC_ALL=C, tr in a UTF-8 locale can behave differently on high bytes.

Questions

Is 12 characters enough?

It meets PCI DSS v4.0.1 and it is comfortably above the 64-bit line NIST uses for rate limiting, at 74.02 bits with all four classes required. It does not meet the NIST SP 800-63B-4 single-factor minimum of 15 characters. If nothing stops you going longer, go longer.

Does a 12-character password satisfy PCI DSS?

On length and composition, yes: requirement 8.3.6 asks for 12 characters with numeric and alphabetic characters, and this page enforces both. PCI compliance as a whole involves much more than the password itself, including rotation for accounts without multi-factor authentication and controls on how the credential is stored and transmitted.

Why do some sites reject a generated password?

Usually a composition rule the draw happened to miss, or a banned character. 29.0% of uniform 12-character draws from this alphabet are missing at least one of the four classes, so a strict checker rejects them. Switching on the class constraint here removes that failure mode. If the rejection persists, the site is probably banning one of the symbols, and a narrower symbol set will get through.

Is 12 random characters better than a four-word passphrase?

It depends entirely on the word list and how the words were chosen. Twelve characters here is 74.51 bits. Four words drawn uniformly from a 7,776-word list is 51.7 bits, so this is stronger; six such words is 77.5 bits, so that is stronger again. What ruins the comparison is words chosen by a person rather than drawn uniformly, which is worth far less than either figure.

Should I still rotate a 12-character password every 90 days?

Only if PCI DSS applies to the account and it is not protected by multi-factor authentication, in which case 90 days is still required. NIST SP 800-63B-4 removed periodic rotation and asks for a change on evidence of compromise instead, on the grounds that scheduled changes push people toward predictable increments.

Related generators