skip to content

Randomness & Hashing

SecureRandom, Digest::SHA256, OpenSSL::HMAC and BCrypt::Password cover tokens, digests, signatures and password hashes in Ruby. Interviewers check you never use rand or == for any of them.

on this pageshow

explore

questions

5

In Ruby, which SecureRandom method would you use for a password-reset token in a URL, and what does its argument count?

level: juniorimportance: must knowfreq 60%

answer

  1. require "securerandom"
  2. urlsafe_base64: - and _, no padding
  3. argument is bytes, not characters
  4. hex(n) gives 2n characters
  5. Random.new.hex is Mersenne Twister

basics

~10 s

SecureRandom.urlsafe_base64(32): URL-safe characters, no padding, bytes from the operating system's secure source. The argument counts random bytes, not characters, so 32 bytes become 43 characters. rand and Random produce predictable output.

solid answer

~40 s

`SecureRandom.urlsafe_base64(32)` returns 32 random bytes encoded as URL-safe Base64 (`A-Z`, `a-z`, `0-9`, `-`, `_`), with no `=` padding unless you pass `true` as the second argument, so it drops straight into a link as a 43-character string. `SecureRandom.hex(32)` is also fine but longer: its argument is bytes too, so it returns 64 hex characters. Both default to 16 bytes when called without an argument, and the docs warn that default may grow, so pass the size explicitly. SecureRandom reads the operating system's generator through `Random.urandom`, with OpenSSL as a fallback. `rand` and `Random` use a seeded Mersenne Twister, and once `securerandom` is loaded even `Random.new.hex` exists, same name, predictable output. Store only a `Digest::SHA256.hexdigest` of the token, expire it, and use it once.

code

ruby · 10 lines
ruby
require "securerandom"
require "digest"

token = SecureRandom.urlsafe_base64(32) # 43 chars, 256 bits
digest = Digest::SHA256.hexdigest(token) # store this, email the token

SecureRandom.hex(32).length              # => 64
SecureRandom.urlsafe_base64(32).length   # => 43

Random.new(1).hex(8) == Random.new(1).hex(8) # => true: same seed, same output

go deeper

for a junior

Recall that tokens come from SecureRandom, never rand, and that urlsafe_base64 and hex take a byte count rather than a length.

for a middle

Explain where SecureRandom's bytes come from, why the same formatter methods on Random are predictable, and how output length relates to the byte count.

for a senior

Design the full reset flow: token size, a stored SHA-256 digest, expiry, single use, and no token in logs.

for a principal

Standardise one token helper for the codebase, so size, encoding and storage choices are made once and reviewed once.

## What SecureRandom is `SecureRandom` is a standard-library module, loaded with `require "securerandom"`, that produces random bytes suitable for **secrets**: session ids, password-reset tokens, API keys, salts. Its bytes come from the operating system's random device through `Random.urandom`; if that is unavailable it falls back to `OpenSSL::Random`, and if neither works it raises `NotImplementedError` rather than returning weak output. The formatting methods all come from the `Random::Formatter` module, which `SecureRandom` extends. Each one takes a size and converts raw random bytes into a string shape: | Method | Argument means | Default | Output for argument 32 | |---|---|---|---| | `random_bytes(n)` | bytes | 16 | 32 raw bytes, any value 0-255 | | `hex(n)` | bytes | 16 | 64 characters, `0-9a-f` | | `base64(n)` | bytes | 16 | 44 characters, may include `+`, `/`, `=` | | `urlsafe_base64(n, padding = false)` | bytes | 16 | 43 characters, `A-Za-z0-9-_` | | `alphanumeric(n)` | **characters** | 16 | 32 characters, `A-Za-z0-9` | | `uuid` | none | none | 36 characters, 122 random bits | Two details trip people up. The size argument of `hex`, `base64` and `urlsafe_base64` counts **bytes of randomness, not output characters**, while `alphanumeric` counts characters. And the defaults are documented as "16 is assumed; it may be larger in the future", so code that relies on the length should pass the size. ## Why not rand or Random - `Kernel#rand`, `Random.rand` and `Random.new(seed)` are backed by a **Mersenne Twister**, a fast generator designed for simulations and tests, where reproducing a sequence from a seed is a feature. - Its outputs are predictable to anyone who observes enough of them or knows the seed, which is exactly the property a reset token must not have. - **The same method names exist on `Random`.** `Random::Formatter` is mixed into `Random` as well, so after `require "securerandom"` the calls `Random.hex`, `Random.new.hex(32)` and `Random.new(42).uuid` all work and look identical to the secure ones. `Random.new(1).hex(8)` returns the same string on every run. What makes the value secure is the **receiver**, `SecureRandom`, not the method name. ## Issuing a reset token 1. **Generate** with `SecureRandom.urlsafe_base64(32)`: 256 bits, safe in a query string without escaping. 2. **Store a digest**, not the token: `Digest::SHA256.hexdigest(token)`. A read-only leak of the table then yields nothing usable. A fast digest is fine here because the input is already high-entropy; passwords are a different case. 3. **Record an expiry** and a used flag next to the digest. 4. **On use**, hash the presented token and look the digest up; mark it used and reject it after expiry. ## Pitfalls - **Counting characters.** `SecureRandom.hex(16)` is 32 characters but only 128 bits; asking for `hex(8)` because "8 looks short" halves the strength. - **Padding in URLs.** `SecureRandom.base64` can contain `+`, `/` and `=`, which need escaping in query strings; `urlsafe_base64` avoids that. - **Seeding for tests.** `srand` and `Random.new(seed)` make `rand` repeatable; they have no effect on `SecureRandom`, which cannot be seeded. - **Truncating output.** Slicing `hex(32)[0, 8]` keeps 32 bits. Ask for the size you need instead of cutting. - **Logging the token.** A token in logs or error trackers is as good as a leaked password until it expires. ## Reading token code in an interview | Code | Verdict | |---|---| | `rand(36**20).to_s(36)` | predictable: Mersenne Twister output | | `Random.new.urlsafe_base64(32)` | predictable: right method name, wrong receiver | | `Digest::SHA256.hexdigest(Time.now.to_s)` | predictable: hashing a guessable input adds no randomness | | `SecureRandom.hex(4)` | secure source, but only 32 bits | | `SecureRandom.random_number(1_000_000)` | secure source, about 20 bits: a one-time code with attempt limits, not a link token | | `SecureRandom.urlsafe_base64(32)` | secure source, 256 bits, URL-safe | The pattern interviewers look for is two separate checks: **where the bytes come from** (only `SecureRandom` is right) and **how many there are** (count bytes, not characters).

  • Why is Random.new.hex(32) wrong for a reset token even though SecureRandom.hex(32) is fine?
    Both use the `hex` method from `Random::Formatter`, but the bytes come from the receiver. `Random` feeds it from a Mersenne Twister, whose output can be predicted or reproduced from a seed; `SecureRandom` feeds it from the operating system's secure generator.
  • Why store Digest::SHA256.hexdigest(token) rather than the token itself?
    If the table leaks read-only, a stored token can be used to reset accounts directly, while its digest cannot be turned back into the token. A fast digest is enough because the token already carries 256 random bits; a slow password hash is meant for low-entropy secrets such as human passwords.

saying these in an interview costs you the question

  • rand(10**20).to_s is fine for a reset token if the number is big enough.
  • SecureRandom.hex(16) returns a 16-character string.
  • Random.new.hex is secure because it is the same method SecureRandom uses.
  • Calling srand with a random seed makes rand safe for tokens.
  • SecureRandom.base64 output can go into a URL without escaping.
open as a page

With the bcrypt gem in Ruby, how do BCrypt::Password.create and BCrypt::Password#== store and check a password, and what does cost control?

level: middleimportance: must knowfreq 52%

basics

~20 s

BCrypt::Password.create(password) returns a 60-character string holding version, cost, a random salt and the hash. To check, wrap the stored string with BCrypt::Password.new and call == with the candidate, which rehashes it with the stored salt and cost.

open as a page

In Ruby, how do you compute and verify an HMAC-SHA256 signature on an incoming API request body with the standard library?

level: middleimportance: should knowfreq 44%

basics

~10 s

Compute OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body), digest name first, then key, then data, and compare it with the received signature using OpenSSL.secure_compare, never ==. Sign the raw body bytes, before any JSON parsing.

open as a page

In Ruby's openssl library, how do OpenSSL.fixed_length_secure_compare and OpenSSL.secure_compare differ, and when would you pick each?

level: seniorimportance: should knowfreq 30%

basics

~10 s

OpenSSL.fixed_length_secure_compare compares equal-length strings in constant time and raises ArgumentError when lengths differ. OpenSSL.secure_compare hashes both inputs with SHA-256 first, so any lengths work and the length is masked, at extra cost.

open as a page

In Ruby, is SecureRandom.uuid or SecureRandom.uuid_v7 a sound password-reset token, and what does each one actually contain?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

SecureRandom.uuid (version 4) carries 122 random bits and is acceptable, though urlsafe_base64(32) is stronger and carries more bits per character. SecureRandom.uuid_v7 starts with a millisecond timestamp and has only 74 random bits, so it leaks creation time and suits identifiers, not secrets.

open as a page