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 caseRecommended sourceNotes
Session tokensOsRngMust be unpredictable
Password reset linksOsRngMust be unguessable
Encryption keysOsRng or a KDFNever use predictable seeds
Nonces for authenticated encryptionOsRng or protocol-specific nonce generationMust satisfy protocol requirements
Test dataStdRng or SmallRngDeterminism is often useful
Shuffling UI elementsthread_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.

EncodingProsCons
HexSimple, URL-safe, easy to debugLarger output
Base64CompactMay need URL-safe variant
Base64 URL-safeCompact and URL-friendlySlightly 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.

  • OsRng is backed by the operating system and is appropriate for secrets.
  • thread_rng() is convenient and often secure enough for many applications, but OsRng is the clearer choice for security-critical generation.
  • StdRng is deterministic once seeded and is best for reproducible tests or simulations.

A practical guideline:

  • use OsRng for secrets
  • use StdRng for 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 uses OsRng
  • 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 StdRng with low-entropy values
  • truncating tokens too aggressively
  • reusing nonces with the same key
  • assuming Debug output 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:

  1. The RNG is cryptographically secure.
  2. The output has enough entropy for the threat model.
  3. The token is encoded safely for its transport channel.
  4. The value is stored and compared securely.
  5. Nonces satisfy the protocol’s uniqueness requirements.
  6. Tests use deterministic RNGs only in test code.
  7. 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.

Learn more with useful resources