Random 32 character string generator

Thirty-two characters from a 62-symbol alphabet: 190.5 bits, the usual size for session tokens and service secrets.

G
More options
up to 10,000

Constraints

190.5 bits, and the point where length stops helping

Thirty-two alphanumeric characters give 32 x log2(62) = 190.534 bits over a keyspace of about 2.27 x 10^57. Every brute-force figure at this length is a number with no physical interpretation, so quoting one would be theatre rather than information.

The useful statement is where the ceiling is. At 128 bits a secret matches the strength of AES-128, and beyond that extra bits buy nothing against brute force, because brute force was already off the table. NIST sets its mandated minimum random-bit-generator security strength at 112 bits, so 190.5 is comfortably past every relevant line.

What that means in practice is that the risk moves. A 32-character secret is not going to be guessed. It is going to leak: committed to a repository, printed in a log line, pasted into a chat, captured in a screenshot of an environment file, or left in a CI variable after the person who set it changed jobs. Length does nothing about any of those, and rotation does.

Past about 128 bits, more characters are free but they are not protection. If you cannot rotate the secret within an hour of finding out it leaked, its length is not the problem you have.

Where the number 32 comes from

Thirty-two is a hexadecimal convention that escaped into general use. An MD5 digest is 32 hexadecimal characters. An AES-128 key is 16 bytes, also written as 32 hexadecimal characters. At 4 bits per character that is exactly 128 bits, which is why so many configuration fields ask for 32 characters without saying what alphabet they mean.

A 32-character alphanumeric string is not that value. It is 190.53 bits, because base62 carries 5.954196 bits per character rather than 4. Both are perfectly good secrets and they are not interchangeable when a specification names a bit length or a byte count. If the field is validated as hexadecimal, generate hexadecimal.

The other place this length is anchored is the shell one-liner that people have been copying for twenty years: reading /dev/urandom, filtering it with tr -dc to an alphanumeric set, and taking the first 32 characters. That one-liner is correct. /dev/urandom is the kernel CSPRNG, and tr discards bytes outside the set rather than folding them into it, so there is no modulo bias. It is in the code panel below along with the openssl equivalent.

Session tokens, service secrets and rotation

This is the length most session identifiers, webhook signing secrets, service-to-service credentials and framework secret keys settle on. At 190.5 bits the token is unguessable in any setting, including one where an attacker can make unlimited attempts against a fast endpoint with no rate limiting, which is the scenario a session identifier actually faces.

Two properties matter more than the bit count here. The first is that the token is compared in constant time, so that a timing side channel does not leak it a byte at a time. The second is that it is stored hashed at rest wherever that is possible, so a database disclosure does not immediately become an authentication bypass.

For a token that goes into a URL, check the alphabet before the length. Alphanumeric is safe everywhere; base64url is safe in a path or a query string; standard base64 is not, because + and / and = all need encoding. That choice causes far more incidents at this length than the difference between 32 and 40 characters ever will.

Doing this in your own code

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

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

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

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

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

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

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 32-character random string overkill?

For guessing resistance, yes, and deliberately so. At 190.53 bits it is 62 bits past the 128-bit point where brute force stopped being the threat. The extra length costs nothing, so there is no reason to trim it, but do not mistake it for extra security against the way secrets actually get lost.

Is a 32-character string 32 bytes?

Only if you mean the storage. As entropy, a 32-character alphanumeric string carries 190.53 bits, and a 32-character hexadecimal string carries exactly 128 bits, which is 16 bytes of underlying key material. The character count and the byte count are different quantities and specifications almost always mean the second.

What should I use for a session token?

32 characters at 190.53 bits is a sound default, or 32 hexadecimal characters if the consuming system wants hex. What matters alongside the length is that the token is compared in constant time, stored hashed where possible, given an expiry, and invalidated on logout.

Is the /dev/urandom one-liner as good as this generator?

Yes, when it is written correctly. LC_ALL=C tr -dc "A-Za-z0-9" reading from /dev/urandom and piped to head -c 32 draws from the kernel CSPRNG and discards out-of-set bytes rather than folding them, so the output is uniform. It is in the code panel below. The reason to use this page instead is that you may not be at a shell.

Does the value leave my browser?

No. Generation happens in the page from crypto.getRandomValues(), nothing is sent, nothing is logged and nothing is stored on a server. You can verify it: open the network panel, generate, and watch it stay empty.

Related generators