Merge pull request #21 from vrescobar/security/fix-weak-pairing-code
fix: use CSPRNG for pairing code generation
This commit is contained in:
commit
6f238541a7
1 changed files with 33 additions and 9 deletions
|
|
@ -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 {
|
fn generate_code() -> String {
|
||||||
use std::collections::hash_map::DefaultHasher;
|
// UUID v4 uses getrandom (backed by /dev/urandom on Linux, BCryptGenRandom
|
||||||
use std::hash::{Hash, Hasher};
|
// on Windows) — a CSPRNG. We extract 4 bytes from it for a uniform random
|
||||||
use std::time::SystemTime;
|
// 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();
|
loop {
|
||||||
SystemTime::now().hash(&mut hasher);
|
let uuid = uuid::Uuid::new_v4();
|
||||||
std::process::id().hash(&mut hasher);
|
let bytes = uuid.as_bytes();
|
||||||
let raw = hasher.finish();
|
let raw = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||||
format!("{:06}", raw % 1_000_000)
|
|
||||||
|
if raw < REJECT_THRESHOLD {
|
||||||
|
return format!("{:06}", raw % UPPER_BOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a cryptographically-adequate bearer token (hex-encoded).
|
/// Generate a cryptographically-adequate bearer token (hex-encoded).
|
||||||
|
|
@ -314,6 +325,19 @@ mod tests {
|
||||||
assert!(code.chars().all(|c| c.is_ascii_digit()));
|
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]
|
#[test]
|
||||||
fn generate_token_has_prefix() {
|
fn generate_token_has_prefix() {
|
||||||
let token = generate_token();
|
let token = generate_token();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue