82.2 secrets: Cryptographically Secure Random Values
Alright, let’s talk about generating secrets. This is the absolute bedrock of almost everything in security. If you’re generating a password, a session token, an encryption key, or a nonce, you need a value that is fundamentally, mathematically unpredictable. You cannot, under any circumstances, just rand() your way out of this problem. The standard random number generators in most languages are designed for speed and statistical distribution for things like simulations or games, not for secrecy. They’re predictable. If an attacker can figure out the seed value, they can recreate the entire sequence of “random” numbers you generated, which means they can forge your session, decrypt your data, or impersonate your user. We need cryptographically secure randomness.
The key difference is the source of entropy. Entropy is the measure of unpredictability. A secure random number generator (CSPRNG) gathers entropy from chaotic, unpredictable hardware events—things like micro-variations in disk read times, keyboard interrupts, or network packet arrival times. This high-quality entropy is used to seed a deterministic algorithm that outputs a stream of bits that looks random but is incredibly hard to predict without knowing that initial seed. It’s the difference between a magician pulling a rabbit from a hat (predictable if you know the trick) and actually conjuring one from the elemental void (genuinely unpredictable).
The Golden Rule: Never Use Math.random() for Secrets
I need you to internalize this. Math.random() in JavaScript, random.randint() in Python, rand() in C++… these are all off-limits for security purposes. They are deterministic algorithms seeded with a value that often has low entropy (like the current time). Let’s be brutally honest: using these for crypto is like building a bank vault out of tissue paper because it was cheaper and easier to cut. The designers of these languages made a choice: fast and good enough for non-security tasks. It’s our job to know when that choice doesn’t apply.
Here’s what you do instead. Every major language provides a dedicated interface for this.
Generating a Secure Random String in Python
Python gives us secrets module, which is literally designed for this exact job. It’s a thin, welcoming wrapper around the gold-standard os.urandom().
import secrets
import string
# Generate a 32-character random URL-safe string, perfect for a token
def generate_secure_token(length=32):
alphabet = string.ascii_letters + string.digits + '-_'
return ''.join(secrets.choice(alphabet) for i in range(length))
token = generate_secure_token()
print(f"Your secure token is: {token}")
Why secrets.choice()? Because it uses the CSPRNG to select from the alphabet. The older method of os.urandom(length) gives you bytes, which you’d then have to encode to text safely (using base64 encoding, for instance). The secrets module just makes the safe way the easy way.
Generating Cryptographic Keys and Bytes in Go
Go’s approach is fantastically direct. The crypto/rand package is your go-to, and it reads from a cryptographically secure source like /dev/urandom on Unix systems.
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
)
func main() {
// Generate 32 random bytes for a 256-bit key
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
panic(err) // In real code, handle this more gracefully. No, really.
}
// Encode it to a printable string to store or transmit
keyBase64 := base64.StdEncoding.EncodeToString(key)
fmt.Printf("Your 256-bit key is: %s\n", keyBase64)
}
Notice the error handling. rand.Read can actually fail if the system can’t gather enough entropy. This is a rare edge case, but in security, we handle edge cases. We don’t panic (well, we might, but our code shouldn’t).
The Classic Pitfall: Seeding Your Own Generator
This is a horror story I see too often. A developer learns that random is bad, so they try to “fix” it by seeding it with a “secure” value from os.urandom(). This is like using a rocket engine to power a children’s toy car. You’re bolting a secure component onto an inherently insecure system. The underlying algorithm is still predictable; you’ve just made the seed unpredictable. This is fragile, unnecessary, and a sign you’re fighting the language’s design. Don’t do it. Use the right tool for the job.
# NO. BAD. WRONG.
import random
import os
random.seed(os.urandom(100)) # Waste 100 bytes of good entropy
bad_key = random.getrandbits(256) # To get a predictably generated number
Just use secrets or your language’s equivalent. It’s not just easier, it’s correct. Your brilliant friend (me) is telling you: this is one area where cutting corners will absolutely, 100%, come back to bite you. Do it right the first time.