From 690e79a39de5ee481519fbfbb84077f5f43aace4 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 24 Aug 2026 13:17:06 +0200 Subject: [PATCH] fix(security): port the RNG onto the rand 0.10 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `main` has not compiled since #133 bumped rand from 0.8.7 to 0.10.2: `rand::rngs::OsRng` and `rand::RngCore` no longer exist under those names, and `src/security.rs` is the crate's only user of either. CI has been red on every commit since 2bddbdd; the split merged on top of it because its branch predated the bump and was never rebased. `OsRng` is now `SysRng`, and it is fallible where the old one panicked on its own. The three callers whose signature already returns `Result<_, SecurityError>` — the instance key, the AEAD nonce and the Argon2 salt — report it as the new `SecurityError::Random`. `generate_token` keeps its infallible shape and therefore its panic, which is what the previous version did anyway: a system CSPRNG that refuses to answer leaves nothing to fall back on, and a token drawn from anything else would be worse than no token. `SysRng` rather than the infallible `ThreadRng`: the original asked for the operating system directly, and a long-lived instance key is the last place to start routing through a userspace buffer instead. fmt, clippy and 42 unit + 46 integration tests pass. Signed-off-by: InstaZDLL --- src/security.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/security.rs b/src/security.rs index 3a40433..fb2dd75 100644 --- a/src/security.rs +++ b/src/security.rs @@ -11,7 +11,7 @@ use chacha20poly1305::{ aead::{Aead, KeyInit}, ChaCha20Poly1305, Nonce, }; -use rand::{rngs::OsRng, RngCore}; +use rand::{rngs::SysRng, TryRng}; use sha2::{Digest, Sha256}; use subtle::ConstantTimeEq; @@ -26,6 +26,8 @@ pub enum SecurityError { PasswordVerify, #[error("instance key must contain exactly 32 bytes")] InvalidInstanceKey, + #[error("system randomness is unavailable")] + Random, #[error("secret encryption failed")] Encrypt, #[error("secret decryption failed")] @@ -51,7 +53,7 @@ impl SecretBox { Ok(bytes) => bytes, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { let mut bytes = vec![0u8; INSTANCE_KEY_BYTES]; - OsRng.fill_bytes(&mut bytes); + fill_random(&mut bytes)?; write_new_secret_file(path, &bytes)?; // Re-read so a concurrent process that won create_new supplies // the key both processes actually use. @@ -89,7 +91,7 @@ impl SecretBox { pub fn encrypt(&self, plaintext: &[u8]) -> Result { let mut nonce = [0u8; NONCE_BYTES]; - OsRng.fill_bytes(&mut nonce); + fill_random(&mut nonce)?; let cipher_nonce = Nonce::from(nonce); let ciphertext = self .cipher @@ -107,12 +109,26 @@ impl SecretBox { } } +/// Random bytes straight from the operating system. +/// +/// `rand` 0.10 renamed `OsRng` to `SysRng` and made it fallible where the +/// previous version panicked on its own. Callers whose signature can carry the +/// failure now report it; [`generate_token`] cannot change shape, and it keeps +/// the panic it already had — a system CSPRNG that refuses to answer leaves +/// nothing to fall back on, and a token drawn from anything else would be +/// worse than no token. +fn fill_random(dst: &mut [u8]) -> Result<(), SecurityError> { + SysRng + .try_fill_bytes(dst) + .map_err(|_| SecurityError::Random) +} + pub fn hash_password(password: &str) -> Result { if password.len() < 12 { return Err(SecurityError::PasswordHash); } let mut salt_bytes = [0u8; 16]; - OsRng.fill_bytes(&mut salt_bytes); + fill_random(&mut salt_bytes)?; let salt = SaltString::encode_b64(&salt_bytes).map_err(|_| SecurityError::PasswordHash)?; Argon2::default() .hash_password(password.as_bytes(), &salt) @@ -129,7 +145,7 @@ pub fn verify_password(password: &str, encoded_hash: &str) -> Result String { let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); + fill_random(&mut bytes).expect("the system CSPRNG must answer"); format!("{prefix}{}", URL_SAFE_NO_PAD.encode(bytes)) }