Merge pull request #21 from vrescobar/security/fix-weak-pairing-code

fix: use CSPRNG for pairing code generation
This commit is contained in:
Argenis 2026-02-14 08:54:46 -05:00 committed by GitHub
commit 6f238541a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -145,17 +145,28 @@ impl PairingGuard {
}
}
/// Generate a 6-digit numeric pairing code.
/// Generate a 6-digit numeric pairing code using cryptographically secure randomness.
fn generate_code() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::SystemTime;
// UUID v4 uses getrandom (backed by /dev/urandom on Linux, BCryptGenRandom
// on Windows) — a CSPRNG. We extract 4 bytes from it for a uniform random
// number in [0, 1_000_000).
//
// Rejection sampling eliminates modulo bias: values above the largest
// multiple of 1_000_000 that fits in u32 are discarded and re-drawn.
// The rejection probability is ~0.02%, so this loop almost always exits
// on the first iteration.
const UPPER_BOUND: u32 = 1_000_000;
const REJECT_THRESHOLD: u32 = (u32::MAX / UPPER_BOUND) * UPPER_BOUND;
let mut hasher = DefaultHasher::new();
SystemTime::now().hash(&mut hasher);
std::process::id().hash(&mut hasher);
let raw = hasher.finish();
format!("{:06}", raw % 1_000_000)
loop {
let uuid = uuid::Uuid::new_v4();
let bytes = uuid.as_bytes();
let raw = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
if raw < REJECT_THRESHOLD {
return format!("{:06}", raw % UPPER_BOUND);
}
}
}
/// Generate a cryptographically-adequate bearer token (hex-encoded).
@ -314,6 +325,19 @@ mod tests {
assert!(code.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn generate_code_is_not_deterministic() {
// Two codes should differ with overwhelming probability. We try
// multiple pairs so a single 1-in-10^6 collision doesn't cause
// a flaky CI failure. All 10 pairs colliding is ~1-in-10^60.
for _ in 0..10 {
if generate_code() != generate_code() {
return; // Pass: found a non-matching pair.
}
}
panic!("Generated 10 pairs of codes and all were collisions — CSPRNG failure");
}
#[test]
fn generate_token_has_prefix() {
let token = generate_token();