Every language
12 langages, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
function randomHex(n) {
const bytes = crypto.getRandomValues(new Uint8Array(n)); // OS CSPRNG
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
}
// base64url — the URL-safe alphabet, padding stripped:
function randomBase64Url(n) {
const bytes = crypto.getRandomValues(new Uint8Array(n));
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
randomHex(32); // 64 hex chars — 256 bits of entropycrypto.randomUUID() is a UUID, not a general token — 122 random bits locked in a fixed 8-4-4-4-12 shape with version/variant bits burned. Math.random() is a seeded PRNG; a token from it is guessable. crypto is global in browsers and Node 19+; older Node needs require('node:crypto').webcrypto.
TSTypeScript
function randomHex(n: number): string {
const bytes = new Uint8Array(n);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
function randomBase64Url(n: number): string {
const bytes = new Uint8Array(n);
crypto.getRandomValues(bytes);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
}The btoa-vs-buffer difference: browsers and Node 16+ have global btoa (which chokes on code points > 0xFF — hence the byte-by-byte string build), while Node-side code can shortcut with Buffer.from(bytes).toString('base64url') and skip the tr/replace entirely. The crypto global typechecks only with lib.dom or @types/node in scope.
GoGo
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
)
func randomHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil { // OS entropy — crypto/rand
return "", err
}
return hex.EncodeToString(b), nil // 2 hex chars per byte
}
func randomBase64URL(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil // -_ alphabet, no padding
}This is crypto/rand — math/rand is NOT for tokens: its stream is reproducible from a seed, and pre-Go-1.20 an unseeded source was deterministic across runs. RawURLEncoding (no trailing =) keeps the token pasteable into URLs and headers.
RsRust
use rand::rngs::OsRng;
use rand::RngCore;
fn random_hex(n: usize) -> String {
let mut buf = vec![0u8; n];
OsRng.fill_bytes(&mut buf); // OS entropy, rejection-sampled, unbiased
buf.iter().map(|b| format!("{b:02x}")).collect()
}The trap is reaching for rand::thread_rng() — a fast seeded ChaCha, fine for simulations, wrong for secrets. Go straight to OsRng (or rand::random::<[u8; 32]>() which also draws OS bytes). The getrandom crate is the thin OS syscall wrapper — pick it when rand's trait machinery is more dependency than you need.
PHPPHP
$apiKey = bin2hex(random_bytes(32)); // 64 hex chars
$sessionId = rtrim(
strtr(base64_encode(random_bytes(32)), '+/', '-_'),
'='
); // base64urlrandom_bytes() is PHP 7+'s CSPRNG (getrandom(2) or /dev/urandom); bin2hex doubles the length — 32 bytes in, 64 chars out. mt_rand() and uniqid() are predictable (uniqid is a timestamp) and must never appear near a token.
PyPython
import secrets
api_key = secrets.token_hex(32) # 64 hex chars — 256 bits
session_id = secrets.token_urlsafe(32) # ~43 base64url chars — 256 bits
# comparing tokens later? constant-time, not ==:
ok = secrets.compare_digest(given, expected)The argument is BYTES of entropy, not output length — token_hex(32) is 64 chars carrying 32 random bytes. The secrets module replaced the old os.urandom-and-hex recipes, and random is NOT for this: the Mersenne Twister's state falls to 624 observed outputs.
C#C#
using System.Security.Cryptography;
static string RandomHex(int n)
{
byte[] bytes = RandomNumberGenerator.GetBytes(n); // .NET 6+: allocates + fills
return Convert.ToHexString(bytes).ToLowerInvariant();
}Watch the RandomNumberGenerator.GetBytes(array) API shape: .NET 6+ has GetBytes(int) returning the filled array, while the older overload fills a preallocated byte[]. Convert.ToHexString emits UPPERCASE — lowercase it for consistency with every other language here. System.Random (even seeded) is predictable; never for tokens.
JvJava
import java.security.SecureRandom;
import java.util.HexFormat;
static final SecureRandom RNG = new SecureRandom();
static String randomHex(int n) {
byte[] bytes = new byte[n];
RNG.nextBytes(bytes);
return HexFormat.of().formatHex(bytes); // Java 17+
}getInstanceStrong() can block on entropy at boot (NativePRNGBlocking on Linux) — plain new SecureRandom() never blocks and is the default choice. Hoist the instance to a static field: SecureRandom is thread-safe and seeding per call is wasted work. HexFormat is Java 17+; older JDKs need a manual %02x loop.
SwSwift
import Foundation
// random(in:) with the default generator uses SystemRandomNumberGenerator,
// which IS the system CSPRNG (arc4random_buf / BCryptGenRandom):
let bytes = (0..<32).map { _ in UInt8.random(in: .min ... .max) }
let token = bytes.map { String(format: "%02x", $0) }.joined()The default RNG is the system CSPRNG in Swift — unlike most languages, the plain random(in:) is already cryptographically secure; `using: .system` only makes it explicit. SecRandomCopyBytes is the lower-level Security.framework call when you want an explicit error code instead of a crash on exhaustion.
KtKotlin
import java.security.SecureRandom
import java.util.HexFormat
private val rng = SecureRandom()
fun randomHex(n: Int): String {
val bytes = ByteArray(n)
rng.nextBytes(bytes)
return HexFormat.of().formatHex(bytes)
}java.util.UUID.randomUUID() is fine as a UUID but 122 bits only — 6 of its 128 bits are version/variant markers, and the 8-4-4-4-12 shape wastes width. For API keys, read real bytes as above; same JVM SecureRandom as the Java recipe.
RbRuby
require 'securerandom'
token = SecureRandom.hex(32) # 64 hex chars
b64 = SecureRandom.base64(32) # padded, may contain + and /
safe = SecureRandom.base64(32).tr('+/', '-_').delete('=') # URL-safeURL-safe variants are a tr('+/', '-_') away — stdlib securerandom ships hex/base64/random_bytes but no urlsafe_base64 (that one is Rails' ActiveSupport). SecureRandom rides OpenSSL's CSPRNG; Random.new.rand is the Mersenne Twister, not a security source.
ZigZig
const std = @import("std");
var buf: [32]u8 = undefined;
std.crypto.random.bytes(&buf); // always OS-backed: getrandom(2), arc4random, RtlGenRandom
const token = std.fmt.bytesToHex(buf, .lower); // [64]u8
std.debug.print("{s}\n", .{token});std.crypto.random is always OS-backed — it never falls back to a seeded PRNG, so there is no wrong generator to accidentally pick. std.Random.DefaultCsprng exists for streaming a seeded CSPRNG, which is the opposite of what a token wants. bytesToHex returns a comptime-length [2*N]u8 — no allocator needed.