Random 16 character string generator

Sixteen characters from a 62-symbol alphabet: 95.3 bits, the default length for API keys and tokens.

G
More options
up to 10,000

Constraints

95.3 bits, and why this is the reference length

Sixteen alphanumeric characters give 16 x log2(62) = 95.267 bits over a keyspace of about 4.77 x 10^28. That places it in the band where a secret needs no rate limiting to be defensible, and it clears the 15-character minimum NIST SP 800-63B-4 sets for single-factor passwords.

The attack numbers stop being interesting here, which is the point. Averaged over the keyspace, unsalted MD5 on one GPU at 220.6 GH/s needs on the order of 10^9 years. A hundred thousand of those GPUs still needs on the order of 10^4 years. Behind a slow hash the figure has no physical meaning at all.

Sixteen is also the length at which the duplicate question disappears for good. The chance of a collision reaches 1% only after about 3.1 x 10^13 strings, which is more values than most systems will ever issue.

Sixteen characters is not 128 bits

This is the mistake worth correcting at this length. An AES-128 key is 16 bytes, which is 128 bits, which is written as 32 hexadecimal characters. A 16-character alphanumeric string is 95.267 bits. The word sixteen appears in both, and they are not the same quantity.

If a specification asks for a 128-bit key or secret, 16 alphanumeric characters is 33 bits short of it. Twenty-two characters from the same alphabet gives 130.99 bits and clears it; 32 hexadecimal characters gives exactly 128. Pick whichever matches the format the far end expects, and never satisfy a bit-length requirement by counting characters.

The same care applies going the other way. A 32-character hexadecimal string is 128 bits, not 32 bytes, because each hex character carries exactly 4 bits.

Sixteen of what, at 62 characters per symbol unless stated
ValueLengthBits
Alphanumeric string16 characters95.27
Alphanumeric string22 characters130.99
Hexadecimal string32 characters128, exactly
AES-128 key16 bytes128, exactly
Canonical UUID36 characters122 random bits of 128

Hexadecimal is 4 bits per character and base64url is 6, both exact. Base62 is 5.954196 bits per character and is exact at no length at all, which is why alphanumeric strings never land on a round bit count.

Class constraints, and what they actually cost

A uniform 16-character alphanumeric draw fails the PCI DSS class check, which wants at least one numeric and one alphabetic character, about 6.0% of the time. If your validator enforces a mix, you need the generator to handle it without quietly damaging the distribution.

The correct method is to generate the whole string and reject it if it does not qualify, then draw again. That is uniform over the set of valid strings by construction, and at this length and alphabet it costs an average of 1.064 attempts. The tool then reports the corrected entropy rather than the unconstrained figure, because a constraint removes strings from the space and the honest number is slightly lower.

The method to avoid is placing one character of each required class at a fixed position and shuffling afterwards. It looks equivalent and is not. Even with an unbiased Fisher-Yates shuffle the output is not uniform over the valid strings, because some valid strings can be produced in more ways than others, and a biased shuffle makes it worse. Either way the entropy is lower than the figure for whole-string rejection, whatever the tool displays. NIST SP 800-63B-4 says composition requirements SHALL NOT be imposed in the first place; where you are forced to satisfy one anyway, this is how to do it without lying about the result.

Doing this in your own code

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

function randomString(length = 16) {
  // 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();  // 16 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(16))

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(16)

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(16);

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(16);
for (int i = 0; i < 16; 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[16];
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 16; echo

# openssl, if you want 16 hexadecimal characters instead
# (8 bytes = 16 hex characters = 64 bits, exactly)
openssl rand -hex 8

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 16 characters enough for an API key?

For most services, yes. 95.27 bits is far beyond guessing at any rate an attacker can reach against a live endpoint, and beyond offline attack for any reasonable hash. Where a key is long-lived, widely distributed and stored by other people, 32 characters at 190.53 bits removes the question entirely.

Is a 16-character string 128 bits?

No. Sixteen alphanumeric characters is 95.267 bits. One hundred and twenty-eight bits is 16 bytes, or 32 hexadecimal characters, or 22 alphanumeric characters. If a specification names a bit length, count bits and not characters.

Does 16 characters meet the NIST password minimum?

Yes. NIST SP 800-63B-4 requires at least 15 characters for a password used as a single factor, written as a SHALL, and 16 clears it. It also clears the PCI DSS v4.0.1 minimum of 12, and satisfies that standard class check on the first draw about 94% of the time.

What does the must-include-each-class option do to the entropy?

It lowers it slightly, because valid strings are a subset of all strings. The correct value is log2 of the count of valid strings, computed by inclusion and exclusion, and the tool shows that number rather than L x log2(N). At 16 characters over 62 symbols the difference is a fraction of a bit.

Should I add symbols to reach a higher bit count?

Only if the far end accepts them. Moving from 62 to 94 printable characters raises 16 characters from 95.27 to 104.87 bits, which is real but smaller than adding two more characters. Symbols also break shell commands, CSV files and URLs unless you pick a set that is safe for the context.

Related generators