Random bytes generator

Raw bytes from the operating system CSPRNG, displayed as space-separated hex pairs so you can count them.

G

What a byte of entropy is worth

A byte holds 8 bits, which is 256 possible values. If those bits come from a cryptographic source then every one of the 256 values is equally likely and the byte carries a full 8 bits of entropy. Bytes are the only unit in this subject where the arithmetic is completely free of rounding: n bytes is 8n bits, always.

That makes the byte the honest unit for talking about a secret. Character counts depend on the alphabet, and everybody quotes them differently. Byte counts do not. When a specification says a key is 256 bits, it means 32 bytes, and how you write those bytes down afterwards is a display decision.

The output here is shown as space-separated hex pairs for exactly that reason. Each pair is one byte, so you can count them without doing any division.

Byte counts, their bit values, and the standards that anchor them
BytesBitsWhat that width means
864NIST SP 800-63B-4 treats a secret below 64 bits as needing rate limiting. This is the floor, not a target.
14112The minimum RBG security strength NIST mandates, via SP 800-131A.
16128An AES-128 key. Past this point brute force is not the attack anyone will use.
24192An AES-192 key.
32256An AES-256 key, and the width of a SHA-256 digest.
64512An HMAC-SHA-512 key at full block width.

Bytes are not characters

A byte is any value from 0 to 255. Only 95 of those are printable ASCII, and 33 of the rest are control codes that will do interesting things to a terminal. You cannot paste raw bytes into a JSON string, a YAML file, an environment variable or a URL. That is the entire reason hex and base64 exist: they are ways of writing bytes down using characters that survive being handled.

Both cost you length. Hex doubles it, at 100% overhead and exactly 4 bits per character. Base64 uses four characters per three bytes, a 33% overhead at exactly 6 bits per character. Neither adds or removes a single bit of entropy. Sixteen random bytes is 128 bits as 16 raw bytes, as 32 hex characters, and as 22 base64url characters.

So when a library asks for a length, read the documentation before you type a number. Node randomBytes, Python token_bytes, Go make([]byte, n), PHP random_bytes and .NET GetBytes all take bytes. Most web forms, including most of our competitors, take characters. The two differ by a factor of two or a factor of 4/3 depending on the encoding, and getting it backwards halves your security margin without any visible symptom.

The failure is silent. A 16-character hex key where you meant 16 bytes is 64 bits, not 128, and it will work perfectly, encrypt correctly and pass every test you write. Nothing tells you until someone brute-forces it.

Where the bytes actually come from

crypto.getRandomValues() fills the array from the operating system CSPRNG. On Linux that is the getrandom syscall drawing on the kernel entropy pool; on Windows it is BCryptGenRandom; on macOS and iOS it is SecRandomCopyBytes. The browser is a thin pass-through, not a generator in its own right, which is why a browser-side tool has no entropy disadvantage against a server-side one.

There is one implementation detail worth knowing because it produces a real error. getRandomValues() refuses a single request over 65,536 bytes and throws QuotaExceededError. Larger requests have to be filled in chunks, and a generator that does not do this simply fails above 64 KiB. Ours loops.

We never fall back to Math.random() if Web Crypto is missing. A silent downgrade to a predictable source is the worst available failure mode for a tool people use to make secrets, so the generator throws instead.

What to do with the bytes once you have them

For a symmetric key, feed the raw bytes to the cipher and never encode them at all. Encoding is only for transport and storage in text. If your key ends up base64-encoded in an environment variable, decode it before use rather than passing the base64 string as the key, which is a mistake that silently reduces a 32-byte key to a 43-character password.

For an identifier or a token, encode and then treat the encoded form as the value. Mixing the two, storing hex in one place and raw bytes in another, is how comparison bugs start.

For a salt or a nonce, the bytes do not need to be secret, only unique and unpredictable. Sixteen bytes is the usual answer; a 12-byte nonce is the AES-GCM standard size and must never repeat under the same key.

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 random bytes do I need for a key?

Sixteen bytes gives 128 bits and matches AES-128; 32 bytes gives 256 bits and matches AES-256. Below 8 bytes you are under the 64 bits that NIST SP 800-63B-4 treats as requiring rate limiting, which makes it unsuitable for anything standing on its own.

Why is the output shown as hex if I asked for bytes?

Because raw bytes cannot be displayed. Most byte values are not printable characters, and several are control codes. The hex pairs are a rendering of the bytes, one pair per byte, chosen so you can count them at a glance. The underlying value is the bytes.

Is this the same as a random string generator?

No. A random string is drawn from an alphabet you choose, so each character carries log2(alphabet size) bits. Random bytes are drawn from all 256 values, so each byte carries exactly 8. If you need something you can paste into a config file, you want a string or an encoded form of these bytes.

Are 32 random bytes really as good as a 43-character password?

They are the same thing. Thirty-two random bytes is 256 bits; 43 base64url characters is 258 bit positions of which 256 carry the value. Encoding never changes the entropy, only the number of characters it takes to write down.

Do these bytes leave my browser?

No. Generation happens in the page, in JavaScript, from crypto.getRandomValues(). Open the Network tab and generate: it stays empty. Seven of the fifteen generators we audited produce their values on a server, which means the value crosses the wire before you see it.

Related generators