Random hex generator
Random hexadecimal from the operating system CSPRNG, with bytes, characters and bits shown side by side.
A 32-character hex string is 16 bytes, not 32
This is the single most expensive misreading in the whole subject, and it costs people half their key material. Hexadecimal encodes one byte as two characters, because a byte holds 8 bits and each hex character carries exactly 4. So a 32-character hex string is 16 bytes and 128 bits. A 64-character hex string is 32 bytes and 256 bits.
The confusion has a specific origin. Almost every library API takes a byte count, and almost every user interface takes a character count, and nothing labels which it is. Run openssl rand -hex 32 expecting a 32-character result and you get 64 characters. Call secrets.token_hex(32) in Python and you get 64 characters. Neither is wrong; both are counting bytes.
This page shows all three numbers at once so the question never arises. Change any one of them and the other two follow.
| Bytes | Hex characters | Bits | Where you see this width |
|---|---|---|---|
| 3 | 6 | 24 | A CSS colour, #RRGGBB |
| 4 | 8 | 32 | A CRC32. Far too small to be a secret. |
| 16 | 32 | 128 | An AES-128 key. An MD5 digest is the same width. |
| 20 | 40 | 160 | A SHA-1 digest, and a full git commit hash. |
| 32 | 64 | 256 | An AES-256 key. A SHA-256 digest is the same width. |
| 64 | 128 | 512 | A SHA-512 digest. |
Hex is exactly 4 bits per character, so entropy is a multiplication with no rounding: 32 hex characters is exactly 128 bits and 64 hex characters is exactly 256. Very few alphabets are this clean.
What hex costs you, and when to use something else
Hex doubles the length of whatever it encodes. Sixteen bytes of key material becomes 32 characters on screen and 32 bytes in your configuration file. Base64 is more efficient at four characters per three bytes, a 33% overhead against hex at 100%, so 32 random bytes is 43 base64url characters against 64 hex ones.
Hex is still usually the right choice, for reasons that have nothing to do with size. It has 16 symbols, all of them unambiguous in every font. It survives being pasted into a shell, a URL, a CSV cell, a JSON string and a YAML value without escaping or a single special case. It is case-insensitive on decode. And every language on earth can turn it back into bytes in one line.
Pick base64 when the value travels in a URL or a header and the length matters. Pick hex when the value will be read by a person, typed by a person, or logged somewhere you do not control.
Hex colour codes are a different job
A CSS hex colour is three bytes: red, green and blue, one byte each, written as #RRGGBB. That is 24 bits, so there are 16,777,216 of them. The four-digit and eight-digit forms add an alpha channel, and the three-digit shorthand doubles each digit, so #abc means #aabbcc and reaches only 4,096 colours.
A uniformly random colour is not the same thing as a good random colour. sRGB coordinates are not perceptually uniform, so drawing three independent bytes clusters heavily in the dark and desaturated region of what your eye actually distinguishes. Half of all random hex colours are muddy. If you want a palette rather than a value, randomise hue in a perceptual space and hold lightness and chroma fixed.
For the case where you genuinely want an arbitrary valid colour, though, three random bytes is exactly right, and that is what this tool gives you at a length of 3.
Case, and where it matters
RFC 4648 section 8 defines the base16 alphabet in uppercase. Practice went the other way: openssl, sha256sum, git and almost every Unix tool emit lowercase, while .NET Convert.ToHexString returns uppercase. Both decode to the same bytes, so for anything you decode the difference is invisible.
It stops being invisible the moment hex is compared as a string rather than as bytes. An ETag check, a checksum manifest, a signature header or a database lookup keyed on a hex column will all fail across a case boundary while the underlying values are identical. Normalise on the way in, and decode to bytes before comparing if you can.
Where these characters come from
Every value on this page is drawn from crypto.getRandomValues(), which is the browser interface to the operating system CSPRNG. Nothing is transmitted: open the Network tab, generate, and watch it stay empty. Of fifteen generators we audited on the first page of Google, five use Math.random() somewhere in their generator logic and seven generate on a server, where the algorithm cannot be checked from outside.
Hex needs no rejection sampling at all, which is a small but real advantage. Sixteen divides 256 exactly, so mapping a random byte onto a hex pair is uniform by construction and there is no bias to remove. Alphabets of 16, 32 and 64 symbols are the only common ones where that is true.
Doing this in your own code
JavaScript (browser and Node 19+)
// The argument is BYTES. 16 bytes renders as 32 hex characters.
function randomHex(bytes) {
const b = crypto.getRandomValues(new Uint8Array(bytes));
return [...b].map((v) => v.toString(16).padStart(2, "0")).join("");
}
randomHex(16); // 32 characters, 128 bits
randomHex(32); // 64 characters, 256 bits
// Node has a shorter path that does the same thing:
// require("node:crypto").randomBytes(16).toString("hex") crypto.getRandomValues() throws above 65536 bytes in a single call, so anything larger has to be filled in chunks.
Python 3.6+
import secrets
secrets.token_bytes(16) # 16 raw bytes
secrets.token_hex(16) # 32 characters, 128 bits
secrets.token_hex(32) # 64 characters, 256 bits
# token_hex takes a byte count, not a character count.
# token_hex(32) is 64 characters. This trips people up constantly. secrets draws from os.urandom, which is the operating system CSPRNG. random.random() is not, and must never be used here.
Go
import (
"crypto/rand"
"encoding/hex"
)
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err) // never carry on after an entropy failure
}
hex.EncodeToString(b) // 32 characters Rust
// getrandom = "0.3"
let mut b = [0u8; 16];
getrandom::fill(&mut b).expect("entropy source unavailable");
let s: String = b.iter().map(|v| format!("{:02x}", v)).collect();
// s.len() == 32 On getrandom 0.2 the same function is named getrandom::getrandom.
PHP 7+
// random_bytes() throws on failure. Never fall back to rand() or mt_rand().
$raw = random_bytes(16);
bin2hex($raw); // 32 characters, 128 bits
strlen($raw); // 16 Java 17+
import java.security.SecureRandom;
import java.util.HexFormat;
byte[] b = new byte[16];
new SecureRandom().nextBytes(b);
HexFormat.of().formatHex(b); // 32 lowercase characters C# / .NET 6+
using System.Security.Cryptography;
byte[] b = RandomNumberGenerator.GetBytes(16);
Convert.ToHexString(b); // 32 characters, UPPERCASE
Convert.ToHexString(b).ToLowerInvariant(); // lowercase Convert.ToHexString returns uppercase. Most Unix tooling emits lowercase, so normalise before comparing strings.
Shell
# The number after -hex is BYTES, not characters.
openssl rand -hex 16 # 32 characters, 128 bits
openssl rand -hex 32 # 64 characters, 256 bits
# Straight from the kernel, no openssl:
head -c 16 /dev/urandom | xxd -p
# Raw bytes to a file:
head -c 32 /dev/urandom > key.bin openssl rand -hex 32 prints 64 characters. If you wanted a 32-character string you asked for twice as much as you meant to.
Questions
How many bytes is a 32-character hex string?
Sixteen. Each hex character carries 4 bits, so two characters make one byte. A 32-character hex string is 16 bytes and exactly 128 bits, which is the size of an AES-128 key.
How long should a random hex key be?
For a symmetric key or an API secret, 32 hex characters gives exactly 128 bits and 64 gives exactly 256. Below 32 characters you are under 128 bits and should have a specific reason. Above 64 characters you are past the point where extra length buys anything against brute force.
Is hex output less secure than base64?
No. The encoding does not touch the entropy. Sixteen random bytes carries 128 bits whether you print it as 32 hex characters or 22 base64url characters. Only the number of bytes matters.
Can I use this for a random colour?
Yes. Set the length to 3 bytes and you get a 6-character value that is a valid #RRGGBB colour. Be aware that uniformly random sRGB values cluster perceptually, so a run of them will look muddier than a hand-picked palette.
Does uppercase or lowercase hex matter?
Not for decoding, which is case-insensitive everywhere. It matters when hex is compared as a string, which happens with ETags, checksum manifests and signature headers. Normalise the case before any string comparison.
Why does openssl rand -hex 32 give me 64 characters?
Because the argument is a byte count. You asked for 32 random bytes, and hex renders each byte as two characters. If you wanted 32 characters, ask for 16 bytes.