From 06f892cbbfd947e4adc222247c142b5206b5d139 Mon Sep 17 00:00:00 2001 From: "Toast (gastown)" Date: Fri, 19 Jun 2026 12:37:13 +0000 Subject: [PATCH] refactor: deduplicate shared utilities into utils module Move constant_time_eq, is_vx2_file, format_size, and HumanBytes from cli.rs and gui.rs into src/utils.rs. Export from lib.rs and update both callers to import from crate::utils. --- neuron-encrypt/src/bin/cli.rs | 47 +---------------------------- neuron-encrypt/src/gui.rs | 45 +++++----------------------- neuron-encrypt/src/lib.rs | 1 + neuron-encrypt/src/utils.rs | 56 +++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 84 deletions(-) create mode 100644 neuron-encrypt/src/utils.rs diff --git a/neuron-encrypt/src/bin/cli.rs b/neuron-encrypt/src/bin/cli.rs index 46a170c..0a7f0a1 100644 --- a/neuron-encrypt/src/bin/cli.rs +++ b/neuron-encrypt/src/bin/cli.rs @@ -8,6 +8,7 @@ use clap_complete::{generate, Shell}; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use neuron_encrypt_core::crypto::{self, ProgressReporter, ThrottledReporter}; use neuron_encrypt_core::error::CryptoError; +use neuron_encrypt_core::utils::{constant_time_eq, is_vx2_file, HumanBytes}; use serde::Serialize; use sha2::{Digest, Sha256}; use zeroize::Zeroizing; @@ -147,29 +148,6 @@ fn read_password(password_file: &Option) -> Zeroizing { } } -fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { - let len_a = a.len(); - let len_b = b.len(); - let max_len = len_a.max(len_b); - - let mut byte_comparison_result = 0; - - for i in 0..max_len { - let byte_a = a.get(i).unwrap_or(&0); - let byte_b = b.get(i).unwrap_or(&0); - byte_comparison_result |= byte_a ^ byte_b; - } - - let len_diff = len_a ^ len_b; - // len_mismatch_flag will be 0 if lengths are equal, 1 if lengths are different - let len_mismatch_flag = (((len_diff | len_diff.wrapping_neg()) >> (usize::BITS - 1)) & 1) as u8; - - // Combine byte comparison result with length mismatch flag - // If either bytes don't match OR lengths don't match, final_result will be non-zero - let final_result = byte_comparison_result | len_mismatch_flag; - - final_result == 0 -} fn read_password_confirmed(password_file: &Option) -> Result, String> { let pass1 = read_password(password_file); @@ -252,23 +230,6 @@ impl Drop for IndProgress { } } -struct HumanBytes(u64); - -impl std::fmt::Display for HumanBytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let b = self.0; - if b < 1024 { - write!(f, "{b} B") - } else if b < 1024 * 1024 { - write!(f, "{:.1} KB", b as f64 / 1024.0) - } else if b < 1024 * 1024 * 1024 { - write!(f, "{:.1} MB", b as f64 / (1024.0 * 1024.0)) - } else { - write!(f, "{:.2} GB", b as f64 / (1024.0 * 1024.0 * 1024.0)) - } - } -} - struct QuietReporter; impl ProgressReporter for QuietReporter { fn report(&self, _progress: f32, _message: &str) {} @@ -386,12 +347,6 @@ fn check_output_exists(path: &Path, force: bool, json: bool) -> Result<(), ExitC Ok(()) } -fn is_vx2_file(path: &Path) -> bool { - path.extension() - .and_then(|e| e.to_str()) - .map(|s| s.eq_ignore_ascii_case("vx2")) - .unwrap_or(false) -} fn emit_json(result: &JsonResult) { match serde_json::to_string(result) { diff --git a/neuron-encrypt/src/gui.rs b/neuron-encrypt/src/gui.rs index ef5e781..fc82667 100644 --- a/neuron-encrypt/src/gui.rs +++ b/neuron-encrypt/src/gui.rs @@ -14,6 +14,7 @@ use eframe::egui::{ }; use neuron_encrypt_core::crypto::{self, ProgressReporter, ThrottledReporter}; use neuron_encrypt_core::error::CryptoError; +use neuron_encrypt_core::utils::{constant_time_eq, format_size, is_vx2_file}; use rand_core::RngCore; use sha2::{Digest, Sha256}; use zeroize::Zeroizing; @@ -160,27 +161,6 @@ pub struct NeuronEncryptApp { cancel_flag: Arc, } -fn is_vx2_file(path: &Path) -> bool { - path.extension() - .and_then(|e| e.to_str()) - .map(|s| s.eq_ignore_ascii_case("vx2")) - .unwrap_or(false) -} - -fn constant_time_eq(a: &str, b: &str) -> bool { - let (ab, bb) = (a.as_bytes(), b.as_bytes()); - let max_len = ab.len().max(bb.len()); - let mut acc: u8 = 0; - - for i in 0..max_len { - let av = ab.get(i).copied().unwrap_or(0); - let bv = bb.get(i).copied().unwrap_or(0); - acc |= av ^ bv; - } - - acc |= (ab.len() ^ bb.len()).min(0xff) as u8; - acc == 0 -} fn truncate_chars(s: &str, n: usize) -> String { let mut out = s.chars().take(n).collect::(); @@ -194,17 +174,6 @@ fn sanitize_text(s: &str) -> String { s.trim().to_owned() } -fn format_size(bytes: u64) -> String { - if bytes < 1024 { - format!("{bytes} B") - } else if bytes < 1024 * 1024 { - format!("{:.1} KB", bytes as f64 / 1024.0) - } else if bytes < 1024 * 1024 * 1024 { - format!("{:.1} MB", bytes as f64 / 1024.0 / 1024.0) - } else { - format!("{:.1} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0) - } -} fn strength_color(strength: Strength) -> Color32 { match strength { @@ -441,7 +410,7 @@ impl NeuronEncryptApp { return; } - if self.mode == Mode::Encrypt && !constant_time_eq(&self.password, &self.confirm_password) { + if self.mode == Mode::Encrypt && !constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()) { self.status = Some("Passphrases do not match.".to_owned()); return; } @@ -535,7 +504,7 @@ impl NeuronEncryptApp { )); return; } - if self.mode == Mode::Encrypt && !constant_time_eq(&self.password, &self.confirm_password) { + if self.mode == Mode::Encrypt && !constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()) { self.status = Some("Passphrases do not match.".to_owned()); return; } @@ -1178,7 +1147,7 @@ impl NeuronEncryptApp { if !self.password.is_empty() && !self.confirm_password.is_empty() - && !constant_time_eq(&self.password, &self.confirm_password) + && !constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()) { ui.add_space(8.0); self.draw_notice( @@ -1239,7 +1208,7 @@ impl NeuronEncryptApp { let mismatch = self.mode == Mode::Encrypt && !self.password.is_empty() && !self.confirm_password.is_empty() - && !constant_time_eq(&self.password, &self.confirm_password); + && !constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()); let disabled = self.password.chars().count() < crypto::MIN_PASSWORD_LEN || mismatch || (self.mode == Mode::Encrypt @@ -1558,7 +1527,7 @@ impl NeuronEncryptApp { if !self.password.is_empty() && !self.confirm_password.is_empty() - && !constant_time_eq(&self.password, &self.confirm_password) + && !constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()) { ui.add_space(8.0); self.draw_notice( @@ -1578,7 +1547,7 @@ impl NeuronEncryptApp { ui.add_space(20.0); let mut enabled = self.password.chars().count() >= crypto::MIN_PASSWORD_LEN; if self.mode == Mode::Encrypt { - enabled &= constant_time_eq(&self.password, &self.confirm_password); + enabled &= constant_time_eq(self.password.as_bytes(), self.confirm_password.as_bytes()); } if let Some(msg) = &self.status { diff --git a/neuron-encrypt/src/lib.rs b/neuron-encrypt/src/lib.rs index 021bd59..6bd831e 100644 --- a/neuron-encrypt/src/lib.rs +++ b/neuron-encrypt/src/lib.rs @@ -1,2 +1,3 @@ pub mod crypto; pub mod error; +pub mod utils; diff --git a/neuron-encrypt/src/utils.rs b/neuron-encrypt/src/utils.rs new file mode 100644 index 0000000..dec1ac7 --- /dev/null +++ b/neuron-encrypt/src/utils.rs @@ -0,0 +1,56 @@ +pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + let len_a = a.len(); + let len_b = b.len(); + let max_len = len_a.max(len_b); + + let mut byte_comparison_result = 0; + + for i in 0..max_len { + let byte_a = a.get(i).unwrap_or(&0); + let byte_b = b.get(i).unwrap_or(&0); + byte_comparison_result |= byte_a ^ byte_b; + } + + let len_diff = len_a ^ len_b; + let len_mismatch_flag = (((len_diff | len_diff.wrapping_neg()) >> (usize::BITS - 1)) & 1) as u8; + + let final_result = byte_comparison_result | len_mismatch_flag; + + final_result == 0 +} + +pub fn is_vx2_file(path: &std::path::Path) -> bool { + path.extension() + .and_then(|e| e.to_str()) + .map(|s| s.eq_ignore_ascii_case("vx2")) + .unwrap_or(false) +} + +pub fn format_size(bytes: u64) -> String { + if bytes < 1024 { + format!("{bytes} B") + } else if bytes < 1024 * 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else if bytes < 1024 * 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / 1024.0 / 1024.0) + } else { + format!("{:.1} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0) + } +} + +pub struct HumanBytes(pub u64); + +impl std::fmt::Display for HumanBytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let b = self.0; + if b < 1024 { + write!(f, "{b} B") + } else if b < 1024 * 1024 { + write!(f, "{:.1} KB", b as f64 / 1024.0) + } else if b < 1024 * 1024 * 1024 { + write!(f, "{:.1} MB", b as f64 / (1024.0 * 1024.0)) + } else { + write!(f, "{:.2} GB", b as f64 / (1024.0 * 1024.0 * 1024.0)) + } + } +}