Random 6 character string generator

Six characters from a 62-symbol alphabet: 35.7 bits, generated in your browser from the operating system CSPRNG.

G
More options
up to 10,000

Constraints

What 35.7 bits is, and what it is not

Six characters drawn uniformly from the 62 alphanumerics give 6 x log2(62) = 35.725 bits of entropy over a keyspace of 62^6 = 56,800,235,584 strings. That figure is exact rather than an estimate, because every character here is drawn by rejection sampling from crypto.getRandomValues() and the draws are independent.

It does not survive an attacker who can guess offline. Against unsalted MD5 at the 220.6 GH/s one current GPU reaches, an average search of this keyspace finishes in about a tenth of a second. Against SHA2-256 at 28.35 GH/s it is about a second. No setting on this page changes that, because the length is the binding constraint, not the alphabet.

The relevant standard agrees. NIST SP 800-63B-4 draws its line at 64 bits: if an authentication secret is shorter than that, the verifier SHALL implement a rate-limiting mechanism. Six characters is 28 bits below the line. Use this length only where guessing is rate limited, or where a correct guess wins the attacker nothing.

Choosing six characters is a namespace decision, not a security decision. If the value protects something, the length is wrong, and the 12-character and 16-character pages are the right places to start.

Duplicates arrive long before the keyspace does

The number that constrains a six-character code in practice is not 56.8 billion, it is the birthday bound. Drawing independently from this space, the chance of at least one duplicate passes 1% at roughly 33,800 codes and 50% at roughly 281,000. A campaign that issues a hundred thousand invite codes will very likely contain a collision.

That is manageable if you plan for it: put a unique index on the column, insert, and regenerate on conflict. It is not manageable if you assume uniqueness because the value came from a random source. The assumption is the bug, not the length.

Chance of at least one duplicate among 6-character alphanumeric codes
Codes issuedChance of a duplicate
1,0000.0009%
10,0000.09%
33,8001%
100,0008.4%
281,00050%
1,000,00099.98%

Where six characters is the right size

Invite codes, coupon codes, short URL slugs and reference numbers that a person reads off one screen and types into another. At six characters the value is short enough to say over the phone, print on a card, and fit in a URL without wrapping. That readability is the entire reason to pick it.

Two choices matter more here than the length does. The first is case. A mixed-case six-character code is case-sensitive, and anything a human retypes will be typed in the wrong case eventually. Uppercase and digits only gives 36 symbols and 31.02 bits, which is worse on paper and better at the support desk. The second is look-alike characters: 0 and O, 1 and l and I. Removing them costs 5.70% of the entropy at any length and removes a whole category of failed redemptions.

If nothing human ever reads the value, six characters has no advantage at all. Ten characters costs you nothing in storage and pushes the 1% duplicate bound out from 33,800 to 130 million.

Doing this in your own code

JavaScript
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  // 62 characters

function randomString(length = 6) {
  // Largest multiple of 62 that fits in a uint32. Anything at or above it
  // is discarded, which is what keeps the mapping uniform.
  const limit = Math.floor(2 ** 32 / ALPHABET.length) * ALPHABET.length;
  const buf = new Uint32Array(1);
  let out = "";
  for (let i = 0; i < length; i++) {
    do { crypto.getRandomValues(buf); } while (buf[0] >= limit);
    out += ALPHABET[buf[0] % ALPHABET.length];
  }
  return out;
}

randomString();  // 6 characters

Without the rejection step, 2^32 does not divide evenly by 62 and the first four characters of the alphabet come up slightly more often.

Python 3.6+
import secrets
import string

ALPHABET = string.ascii_letters + string.digits  # 62 characters

"".join(secrets.choice(ALPHABET) for _ in range(6))

secrets.choice goes through randbelow, which rejects out-of-range values internally. random.choice is unbiased as well, but it runs on the Mersenne Twister and must not be used here.

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

const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

func RandomString(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
}

RandomString(6)

crypto/rand.Int already does rejection sampling, so the result is uniform. math/rand is not a substitute at any size.

PHP 7+
function random_string(int $length): string {
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    $max = strlen($alphabet) - 1;
    $out = '';
    for ($i = 0; $i < $length; $i++) {
        $out .= $alphabet[random_int(0, $max)];
    }
    return $out;
}

random_string(6);

random_int is CSPRNG-backed and unbiased. rand(), mt_rand() and array_rand() are none of those things.

Java
import java.security.SecureRandom;

static final String ALPHABET =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static final SecureRandom RNG = new SecureRandom();

StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < 6; i++) {
    sb.append(ALPHABET.charAt(RNG.nextInt(ALPHABET.length())));
}
sb.toString();

nextInt(bound) rejects and redraws when the bound does not divide the range evenly, so no bias is introduced here.

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

const string Alphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

var chars = new char[6];
for (int i = 0; i < chars.Length; i++)
{
    chars[i] = Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)];
}
string value = new string(chars);

RandomNumberGenerator.GetInt32 is unbiased. System.Random is not cryptographic and is the wrong class for this job.

Shell
# Linux and macOS. LC_ALL=C stops tr rejecting bytes outside the locale.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 6; echo

# openssl, if you want 6 hexadecimal characters instead
# (3 bytes = 6 hex characters = 24 bits, exactly)
openssl rand -hex 3

tr -dc discards bytes outside the set rather than folding them into it, so the one-liner is uniform. /dev/urandom is the kernel CSPRNG on both platforms.

Questions

Is a 6-character random string secure?

Not against offline guessing. At 35.7 bits it falls in well under a second against a fast unsalted hash on a single GPU. It is safe only where an attacker has to make each guess through a service you control and that service rate limits them, which is exactly the condition NIST SP 800-63B-4 attaches to any secret below 64 bits.

How many 6-character codes can I issue before duplicates?

The chance of at least one duplicate reaches 1% at about 33,800 codes and 50% at about 281,000. Those figures come from the birthday bound over 62^6, not from exhausting the space. Enforce uniqueness with a database constraint and regenerate on conflict rather than trusting the odds.

Should I exclude 0, O, 1, l and I?

If a person will type the code, yes. It drops the alphabet from 62 to 49 characters and costs 5.70% of the entropy, which at this length you were not relying on anyway. If the code is only ever handled by machines, keep the full alphabet.

Can I use a 6-character string as a one-time password?

Only with strict rate limiting and a short expiry, which is the same condition that makes a 6-digit TOTP code acceptable at 19.93 bits. Six alphanumeric characters is stronger than a 6-digit code by about 15.8 bits, but both live or die on the verifier limiting attempts.

Why does the generator not just pick from Math.random()?

Math.random() is not a cryptographic source and its internal state can be recovered from a handful of outputs, so codes generated after the ones an attacker has seen become predictable. Every value on this site comes from crypto.getRandomValues(), which is backed by the operating system CSPRNG.

Related generators