Random hex string generator
Hexadecimal characters, four bits each with no rounding. Thirty-two characters is exactly 128 bits.
More options
The one alphabet where the arithmetic is exact
Hexadecimal has sixteen symbols and sixteen is 2 to the 4, so each character carries exactly four bits. No logarithm to round, no fractional bit left over, and no rejection sampling cost: a four-bit draw maps onto the alphabet with a single mask and every value is accepted. The same is true of the other power-of-two alphabets, 32 and 64, and of nothing else in common use.
That exactness is why hex is the notation for anything measured in bytes. Thirty-two hex characters is exactly 128 bits, which is an AES-128 key or an MD5 digest. Sixty-four is exactly 256 bits, which is an AES-256 key or a SHA-256 digest. Forty is a SHA-1 digest. Twelve is a MAC address without its separators. None of these lengths is arbitrary, and none of them needs a caveat about approximation.
| Characters | Bytes | Bits | What has this length |
|---|---|---|---|
| 7 | 3.5 | 28 | The default abbreviated git commit id |
| 12 | 6 | 48 | A MAC address with the separators removed |
| 32 | 16 | 128 | MD5 digest, AES-128 key, a UUID without hyphens |
| 40 | 20 | 160 | SHA-1 digest, a full git commit id |
| 64 | 32 | 256 | SHA-256 digest, AES-256 key |
| 128 | 64 | 512 | SHA-512 digest |
A 32-character hex string is 16 bytes, not 32
This is the most common misunderstanding in the whole cluster, and it matters because it is off by a factor of two in the direction that makes a key look twice as strong as it is. Each hex character encodes four bits, so it takes two of them to represent one byte. Thirty-two hex characters therefore encode sixteen bytes and carry 128 bits of entropy.
The confusion has a real source: the string itself occupies 32 bytes when it is written out as ASCII. Both statements are true and they are about different things. The value carries 128 bits. Its textual representation costs 256 bits of storage or bandwidth. Hex is a 2x expansion, exactly, always.
So when a library asks for a byte count and a form asks for a character count, they are not the same number. Python secrets.token_hex(32) returns a 64-character string. openssl rand -hex 16 returns 32 characters. The tool above shows the character count, the byte count and the bit count together for this reason.
If you need 256 bits of key material, that is 64 hex characters, not 32. Halving the byte count is the error that turns an AES-256 key into an AES-128 key without anything visibly going wrong.
Case, and what RFC 4648 actually says
RFC 4648 section 8 defines base16 with the uppercase alphabet, and states that decoders may accept lowercase. Practice went the other way. Git, OpenSSL, the Python and Go standard libraries, and every common digest tool emit lowercase, so lowercase is the de facto convention and the default here.
The trap is comparison. Two hex strings representing the same bytes will not be equal under a byte-for-byte string comparison if their cases differ, and .NET is the notable ecosystem that defaults the other way: Convert.ToHexString returns uppercase. Compare decoded bytes, or normalise the case explicitly before comparing. Doing this with a constant-time comparison also matters wherever the value is a secret.
Hex costs you twice the characters for the same strength
Four bits per character is exact but it is also low. Every other encoding in ordinary use carries more, so if the field the value lands in has a length limit, hex is the wrong choice. The table below is the same 128 bits expressed four ways.
The rule of thumb: use hex when the value is fundamentally a sequence of bytes that someone will need to read, paste or compare against a digest. Use Base64url or a 62-character alphabet when the value is an opaque token and the length matters. Use a 32-character unambiguous alphabet when a person has to retype it.
| Encoding | Alphabet size | Bits per character | Characters for 128 bits |
|---|---|---|---|
| Hex | 16 | 4.000000 | 32 |
| Base32 | 32 | 5.000000 | 26 |
| Alphanumeric | 62 | 5.954196 | 22 |
| Base64url | 64 | 6.000000 | 22 |
Doing this in your own code
JavaScript
// 16 bytes rendered as 32 lowercase hex characters. Exactly 128 bits.
const bytes = crypto.getRandomValues(new Uint8Array(16));
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
// padStart is not optional. Without it a byte below 0x10 renders as one
// character and the string comes out short and non-uniform. On Node, Buffer.from(bytes).toString("hex") renders the same string, and randomBytes(16).toString("hex") from node:crypto does both steps in one call.
Python 3.6+
import secrets
secrets.token_hex(16) # 32 hex characters, 128 bits
secrets.token_hex(32) # 64 hex characters, 256 bits
# The argument is BYTES, not characters. token_hex(32) is 64 characters long. Go
import (
"crypto/rand"
"encoding/hex"
)
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
s := hex.EncodeToString(b) // 32 characters, lowercase PHP 7+
<?php
bin2hex(random_bytes(16)); // 32 hex characters, 128 bits
bin2hex(random_bytes(32)); // 64 hex characters, 256 bits Java 17+
import java.security.SecureRandom;
import java.util.HexFormat;
byte[] b = new byte[16];
new SecureRandom().nextBytes(b);
String s = HexFormat.of().formatHex(b); // lowercase
String u = HexFormat.of().withUpperCase().formatHex(b); C# / .NET 5+
using System.Security.Cryptography;
byte[] b = RandomNumberGenerator.GetBytes(16);
string s = Convert.ToHexString(b); // UPPERCASE
string lower = Convert.ToHexString(b).ToLowerInvariant(); Convert.ToHexString returns uppercase. Most other ecosystems default to lowercase, which is a common round-trip surprise.
Shell
openssl rand -hex 16 # 32 characters
openssl rand -hex 32 # 64 characters
head -c 16 /dev/urandom | xxd -p
head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' Questions
How many hex characters do I need for a 256-bit key?
Sixty-four. Each hex character is exactly four bits, so 256 divided by 4 is 64 characters, which is 32 bytes. For 128 bits it is 32 characters and 16 bytes.
Should hex output be uppercase or lowercase?
Lowercase, unless something downstream requires otherwise. RFC 4648 section 8 defines base16 in uppercase and allows decoders to accept lowercase, but git, OpenSSL and most standard libraries emit lowercase, so lowercase is what people expect. The one common exception is .NET, whose Convert.ToHexString returns uppercase.
Is a random hex string the same as a hash?
No. A hash is a deterministic function of an input and is only as unpredictable as that input. A random hex string comes from the operating system CSPRNG and has no input at all. They often look identical because both are rendered as hex of the same length, which is precisely why a hash of a guessable value makes such a poor token.
Does generating hex waste entropy through rejection?
No. Sixteen is a power of two, so a four-bit draw maps onto the alphabet with a mask and nothing is ever rejected. This is one of only three alphabet sizes in common use where that holds, along with 32 and 64.
Can I use a hex string as a database primary key?
You can, but store the bytes rather than the text where the database supports it. A 32-character hex string in a varchar column costs 32 bytes plus overhead and compares character by character, while the same value in a 16-byte binary or uuid column costs half as much and indexes better. Where the identifier is a UUID, use the native uuid type.