
Preventing Insecure Randomness in Rust: Generating Tokens, Keys, and Nonces Safely
Why insecure randomness is a security problem
Random values are often used as security boundaries. If an attacker can predict them, they may be able to:
- guess session tokens and impersonate users
- brute-force password reset links
- reuse or predict nonces in cryptographic protocols
- weaken key generation and secret material
- correlate supposedly unguessable identifiers
A common mistake is to treat all randomness as equivalent. It is not. Rust offers both general-purpose pseudo-random number generation and cryptographically secure randomness. For security-sensitive data, you must use the latter.
Use a cryptographically secure RNG
For security purposes, prefer the rand crate’s OS-backed secure generator through OsRng. It draws entropy from the operating system and is designed for secrets, not simulation.
Add the dependency:
[dependencies]
rand = "0.8"Generate a secure token:
use rand::rngs::OsRng;
use rand::RngCore;
fn generate_token() -> [u8; 32] {
let mut token = [0u8; 32];
OsRng.fill_bytes(&mut token);
token
}This is appropriate for raw secret bytes. If you need a human-readable token, encode the bytes using hex or base64.
use rand::rngs::OsRng;
use rand::RngCore;
fn generate_api_key() -> String {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
hex::encode(bytes)
}Add hex encoding support:
[dependencies]
hex = "0.4"
rand = "0.8"When to use secure randomness vs. ordinary randomness
Not every random value must be cryptographically secure. The distinction matters because secure RNGs can be slower and are sometimes unnecessary.
| Use case | Recommended source | Notes |
|---|---|---|
| Session tokens | OsRng | Must be unpredictable |
| Password reset links | OsRng | Must be unguessable |
| Encryption keys | OsRng or a KDF | Never use predictable seeds |
| Nonces for authenticated encryption | OsRng or protocol-specific nonce generation | Must satisfy protocol requirements |
| Test data | StdRng or SmallRng | Determinism is often useful |
| Shuffling UI elements | thread_rng() | Usually fine if not security-sensitive |
A useful rule: if the value protects identity, access, or cryptographic integrity, use secure randomness.
Avoid seeding predictable generators with weak inputs
A frequent vulnerability is using a deterministic RNG seeded with time, process ID, or another low-entropy source. That may look random during development but is often guessable in production.
Bad example:
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::time::{SystemTime, UNIX_EPOCH};
fn bad_token() -> u64 {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut rng = StdRng::seed_from_u64(seed);
rng.gen()
}This is insecure because an attacker can narrow the seed to a small time window and reproduce the output.
Use OsRng instead:
use rand::rngs::OsRng;
use rand::RngCore;
fn secure_u64() -> u64 {
OsRng.next_u64()
}Generate tokens with enough entropy
A secure token must be long enough that brute force is impractical. The required length depends on the threat model, but these are good defaults:
- 16 bytes: acceptable for low-risk identifiers, but often too short for secrets
- 32 bytes: strong default for API keys and reset tokens
- 64 bytes: useful when you want extra margin or plan to truncate after encoding
For most web applications, 32 random bytes encoded as hex or base64 is a solid choice.
Hex vs. base64
Hex is simple and URL-safe, but doubles the size of the raw bytes. Base64 is more compact, but standard base64 includes +, /, and = padding, which may require URL encoding.
| Encoding | Pros | Cons |
|---|---|---|
| Hex | Simple, URL-safe, easy to debug | Larger output |
| Base64 | Compact | May need URL-safe variant |
| Base64 URL-safe | Compact and URL-friendly | Slightly less familiar |
If the token will appear in URLs, consider URL-safe base64:
[dependencies]
base64 = "0.22"
rand = "0.8"use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use rand::rngs::OsRng;
use rand::RngCore;
fn generate_url_token() -> String {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}Use secure randomness correctly in application flows
Password reset tokens
A password reset token should be:
- generated with
OsRng - long enough to resist guessing
- stored server-side in hashed form if possible
- single-use and time-limited
Example:
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use rand::rngs::OsRng;
use rand::RngCore;
fn new_reset_token() -> String {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}Store the token with an expiration timestamp and invalidate it after successful use. If your database is compromised, hashing the token before storage reduces exposure.
Session identifiers
Session IDs must be unpredictable and unique. Do not derive them from user IDs, timestamps, or counters. A secure random 32-byte value is a good default.
Nonces and IVs
Some cryptographic modes require unique nonces, not necessarily secret ones. Others require both uniqueness and unpredictability. Read the algorithm’s requirements carefully.
For example:
- AEAD schemes often require a unique nonce per key
- some protocols tolerate random nonces
- others require counters or structured nonces
If the protocol says “random nonce,” generate it with OsRng. If it says “unique nonce,” ensure uniqueness even if the value is not secret.
Be careful with thread_rng() and StdRng
Rust’s rand ecosystem includes multiple RNGs, but they serve different purposes.
OsRngis backed by the operating system and is appropriate for secrets.thread_rng()is convenient and often secure enough for many applications, butOsRngis the clearer choice for security-critical generation.StdRngis deterministic once seeded and is best for reproducible tests or simulations.
A practical guideline:
- use
OsRngfor secrets - use
StdRngfor tests and deterministic workflows - use
thread_rng()only when you understand the security properties and do not need explicit OS-backed generation
Design APIs that make secure randomness the default
Security bugs often happen when an API makes the unsafe path easier than the safe one. If you are building a library or internal service, design your interface so callers cannot accidentally choose weak randomness.
Good patterns:
- expose a
generate_secret()function that always usesOsRng - avoid accepting a seed unless deterministic behavior is truly required
- keep token length fixed or validated
- return opaque types instead of raw integers when possible
Example wrapper type:
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use rand::rngs::OsRng;
use rand::RngCore;
pub struct ApiToken(String);
impl ApiToken {
pub fn new() -> Self {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
Self(URL_SAFE_NO_PAD.encode(bytes))
}
pub fn as_str(&self) -> &str {
&self.0
}
}This makes it harder for callers to bypass secure generation.
Handle failures from the OS RNG
Secure randomness can fail, especially in constrained environments, container startup, or unusual platform configurations. Do not assume token generation is infallible.
If your application can recover, return a Result:
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use rand::rngs::OsRng;
use rand::RngCore;
fn try_generate_token() -> Result<String, rand::Error> {
let mut bytes = [0u8; 32];
OsRng.try_fill_bytes(&mut bytes)?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}If failure is unrecoverable in your context, fail fast with a clear error path rather than silently falling back to an insecure generator.
Test securely without weakening production code
Tests often need deterministic values, but production code must remain secure. Keep those concerns separate.
A good pattern is dependency injection:
use rand::RngCore;
fn fill_secret<R: RngCore>(rng: &mut R) -> [u8; 32] {
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
bytes
}In production, call it with OsRng. In tests, use a seeded deterministic RNG.
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn token_generation_is_deterministic_in_tests() {
let mut rng = StdRng::seed_from_u64(42);
let a = fill_secret(&mut rng);
let b = fill_secret(&mut rng);
assert_ne!(a, b);
}
}This keeps tests reproducible without compromising production security.
Common mistakes to avoid
- using timestamps, counters, or usernames as tokens
- seeding
StdRngwith low-entropy values - truncating tokens too aggressively
- reusing nonces with the same key
- assuming
Debugoutput is safe for secrets - falling back to insecure randomness if secure generation fails
- generating secrets in one format and comparing them in another without normalization
A token that looks random is not necessarily secure. Only the entropy source and the generation process determine whether it is safe to use.
Practical checklist
Before shipping code that generates secrets, verify the following:
- The RNG is cryptographically secure.
- The output has enough entropy for the threat model.
- The token is encoded safely for its transport channel.
- The value is stored and compared securely.
- Nonces satisfy the protocol’s uniqueness requirements.
- Tests use deterministic RNGs only in test code.
- No insecure fallback exists if secure generation fails.
If you can answer “yes” to each item, your randomness design is likely robust enough for production use.
