From 1daf61c4fca99d555dfca010beb0ee542dd4da91 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 19:48:15 +0000 Subject: [PATCH] =?UTF-8?q?Security=20Scan=20and=20Hardening=20=E2=80=94?= =?UTF-8?q?=202026-05-09?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed: - Improved timing side-channel protection in CLI by replacing early-exit constant-time comparison. - Ensured Unicode-aware password strength estimation in GUI using character property methods. - Fixed potential UTF-8 slicing panics in GUI by using safe string suffix removal. - Hardened error handling in CLI by replacing generic unwrap calls with descriptive panic messages. - Migrated GUI and Main to egui 0.33 API. Deferred to SECURITY_DEFER.md: - Insufficient Argon2id memory cost (64MB vs 256MB) in no-touch src/crypto.rs. - Unhandled unwrap calls in no-touch src/crypto.rs. - Unmaintained 'paste' dependency. Co-authored-by: darkmaster0345 <152901560+darkmaster0345@users.noreply.github.com> --- SECURITY_DEFER.md | 30 +++++++++++++++++++ audit_output.json | 1 + neuron-encrypt/src/bin/cli.rs | 40 ++++++++++++++++++-------- neuron-encrypt/src/gui.rs | 54 ++++++++++++++++++----------------- neuron-encrypt/src/main.rs | 2 +- 5 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 SECURITY_DEFER.md create mode 100644 audit_output.json diff --git a/SECURITY_DEFER.md b/SECURITY_DEFER.md new file mode 100644 index 0000000..e7bee45 --- /dev/null +++ b/SECURITY_DEFER.md @@ -0,0 +1,30 @@ +[ARGON2ID_PARAMS] - Insufficient Argon2id memory cost +Severity: Critical +Location: neuron-encrypt/src/crypto.rs:114 +Description: The current Argon2id memory cost is set to 64MB (m_cost=65536). Security guidelines recommend a minimum of 256MB (m_cost=262144) for hardened file encryption to resist specialized hardware-based brute-force attacks. +Why not fixed: no-touch zone +Suggested fix: Update Argon2id Params in derive_key and derive_key_v3 to use m_cost=262144. +First detected: 2026-05-09 +Last attempted: 2026-05-09 +Attempt count: 1 +--- +[UNEXPECTED_PANICS] - Unhandled unwrap() calls in crypto-adjacent paths +Severity: Medium +Location: neuron-encrypt/src/crypto.rs:116 +Description: Multiple uses of .unwrap() or .unwrap_or_else() on locks/IO results within src/crypto.rs could lead to panics in production if environment conditions change (e.g., mutex poisoning or unexpected IO failure during file processing). +Why not fixed: no-touch zone +Suggested fix: Replace .unwrap() with proper error handling using CryptoError or .expect() with descriptive panic messages. +First detected: 2026-05-09 +Last attempted: 2026-05-09 +Attempt count: 1 +--- +[RUSTSEC-2024-0436] - Unmaintained dependency: paste +Severity: Medium +Location: Cargo.lock +Description: The `paste` crate is no longer maintained. While it's a macro-utility crate, unmaintained dependencies increase the long-term risk of unpatched vulnerabilities or incompatibility. +Why not fixed: no-touch zone (Cargo.lock modification restricted) +Suggested fix: Replace `paste` with `pastey` or `with_builtin_macros`. +First detected: 2026-05-09 +Last attempted: 2026-05-09 +Attempt count: 1 +--- diff --git a/audit_output.json b/audit_output.json new file mode 100644 index 0000000..cf1bc96 --- /dev/null +++ b/audit_output.json @@ -0,0 +1 @@ +{"database":{"advisory-count":1068,"last-commit":"881a159d8d70075fe79eb23605e7127a9c2a738a","last-updated":"2026-05-07T10:56:41+02:00"},"lockfile":{"dependency-count":537},"settings":{"target_arch":[],"target_os":[],"severity":null,"ignore":[],"informational_warnings":["unmaintained","unsound","notice"]},"vulnerabilities":{"found":false,"count":0,"list":[]},"warnings":{"unmaintained":[{"kind":"unmaintained","package":{"name":"paste","version":"1.0.15","source":"registry+https://github.com/rust-lang/crates.io-index","checksum":"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a","replace":null},"advisory":{"id":"RUSTSEC-2024-0436","package":"paste","title":"paste - no longer maintained","description":"The creator of the crate `paste` has stated in the [`README.md`](https://github.com/dtolnay/paste/blob/master/README.md) \nthat this project is not longer maintained as well as archived the repository\n\n## Possible Alternative(s)\n\n- [`pastey`]: a fork of paste and is aimed to be a drop-in replacement with additional features for paste crate\n- [`with_builtin_macros`]: crate providing a [superset of `paste`'s functionality including general `macro_rules!` eager expansions](https://docs.rs/with_builtin_macros/0.1.0/with_builtin_macros/macro.with_eager_expansions.html) and `concat!`/`concat_idents!` macros\n\n[`pastey`]: https://crates.io/crates/pastey\n[`with_builtin_macros`]: https://crates.io/crates/with_builtin_macros","date":"2024-10-07","aliases":[],"related":[],"collection":"crates","categories":[],"keywords":[],"cvss":null,"informational":"unmaintained","references":[],"source":null,"url":"https://github.com/dtolnay/paste","withdrawn":null,"license":"CC0-1.0","expect-deleted":false},"affected":null,"versions":{"patched":[],"unaffected":[]}}]}} diff --git a/neuron-encrypt/src/bin/cli.rs b/neuron-encrypt/src/bin/cli.rs index ac7121c..6c4c9ba 100644 --- a/neuron-encrypt/src/bin/cli.rs +++ b/neuron-encrypt/src/bin/cli.rs @@ -142,14 +142,27 @@ fn read_password(password_file: &Option) -> Zeroizing { } fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { - return false; - } - let mut diff = 0u8; - for (&x, &y) in a.iter().zip(b.iter()) { - diff |= x ^ y; + 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; } - diff == 0 + + 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> { @@ -185,18 +198,18 @@ impl IndProgress { ProgressStyle::with_template( "[{bar:10.cyan/blue}] {percent}% | {msg} | {bytes:>7}/{total_bytes:7} | {bytes_per_sec} | ETA {eta}", ) - .unwrap() + .expect("Invalid progress bar template") .with_key("bytes_per_sec", |state: &ProgressState, w: &mut dyn std::fmt::Write| { - write!(w, "{}/s", HumanBytes(state.pos())).unwrap(); + write!(w, "{}/s", HumanBytes(state.pos())).expect("Failed to format bytes per second"); }) .progress_chars("##-") } else { ProgressStyle::with_template("{spinner} | {msg} | {bytes} | {bytes_per_sec}") - .unwrap() + .expect("Invalid progress bar spinner template") .with_key( "bytes_per_sec", |state: &ProgressState, w: &mut dyn std::fmt::Write| { - write!(w, "{}/s", HumanBytes(state.pos())).unwrap(); + write!(w, "{}/s", HumanBytes(state.pos())).expect("Failed to format bytes per second"); }, ) .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) @@ -331,7 +344,10 @@ fn is_vx2_file(path: &Path) -> bool { } fn emit_json(result: &JsonResult) { - println!("{}", serde_json::to_string(result).unwrap()); + println!( + "{}", + serde_json::to_string(result).expect("Failed to serialize JSON result") + ); } fn compute_sha256(path: &Path) -> Result { diff --git a/neuron-encrypt/src/gui.rs b/neuron-encrypt/src/gui.rs index 42080b9..855172c 100644 --- a/neuron-encrypt/src/gui.rs +++ b/neuron-encrypt/src/gui.rs @@ -239,10 +239,10 @@ fn eval_strength(password: &str) -> (Strength, f32, &'static str) { let len = password.chars().count(); for c in password.chars() { - if c.is_ascii_uppercase() { + if c.is_uppercase() { has_upper = true; } - if c.is_ascii_digit() { + if c.is_numeric() { has_digit = true; } if !c.is_alphanumeric() { @@ -289,14 +289,9 @@ fn preview_output_name(mode: Mode, path: &Path) -> String { match mode { Mode::Encrypt => format!("{}{}", name, crypto::EXTENSION), Mode::Decrypt => { - let lower = name.to_lowercase(); - if lower.ends_with(crypto::EXTENSION) { - // Use char-boundary-safe slicing - let strip_len = name.len() - crypto::EXTENSION.len(); - name[..strip_len].to_owned() - } else { - name - } + name.strip_suffix(crypto::EXTENSION) + .map(|s| s.to_owned()) + .unwrap_or(name) } } } @@ -760,7 +755,12 @@ impl NeuronEncryptApp { }; painter.rect_filled(button_rect, 7.0, fill); - painter.rect_stroke(button_rect, 7.0, Stroke::new(1.0, stroke_color)); + painter.rect_stroke( + button_rect, + 7.0, + Stroke::new(1.0, stroke_color), + egui::StrokeKind::Middle, + ); painter.text( button_rect.center(), Align2::CENTER_CENTER, @@ -784,10 +784,10 @@ impl NeuronEncryptApp { fn draw_screen_header(&self, ui: &mut egui::Ui) { let rounding = 11.0; let (badge, fill, text_color) = self.screen_badge(); - egui::Frame::none() + egui::Frame::new() .fill(fill) .stroke(Stroke::new(1.0, Palette::BORDER_MED)) - .rounding(rounding) + .corner_radius(rounding) .inner_margin(6.0) .show(ui, |ui| { ui.label( @@ -845,7 +845,7 @@ impl NeuronEncryptApp { }; painter.rect_filled(rect, 8.0, fill); - painter.rect_stroke(rect, 8.0, Stroke::new(1.0, stroke_color)); + painter.rect_stroke(rect, 8.0, Stroke::new(1.0, stroke_color), egui::StrokeKind::Middle); painter.text( rect.center(), Align2::CENTER_CENTER, @@ -889,6 +889,7 @@ impl NeuronEncryptApp { Palette::BORDER_MED }, ), + egui::StrokeKind::Middle, ); painter.text( rect.center_top() + vec2(0.0, 42.0), @@ -969,10 +970,10 @@ impl NeuronEncryptApp { .unwrap_or_else(|_| String::from("Unknown size")); let output = preview_output_name(self.mode, &path); - egui::Frame::none() + egui::Frame::new() .fill(Palette::SURFACE_1) .stroke(Stroke::new(1.0, Palette::BORDER)) - .rounding(10.0) + .corner_radius(10.0) .inner_margin(14.0) .show(ui, |ui| { ui.horizontal(|ui| { @@ -1083,11 +1084,12 @@ impl NeuronEncryptApp { Palette::BORDER }, ), + egui::StrokeKind::Middle, ); let input_rect = Rect::from_min_max(rect.min + vec2(12.0, 11.0), rect.max - vec2(12.0, 11.0)); - ui.allocate_ui_at_rect(input_rect, |ui| { + ui.scope_builder(egui::UiBuilder::new().max_rect(input_rect), |ui| { let edit = if primary { egui::TextEdit::singleline(&mut *self.password).id(id) } else { @@ -1154,10 +1156,10 @@ impl NeuronEncryptApp { border: Color32, text: Color32, ) { - egui::Frame::none() + egui::Frame::new() .fill(fill) .stroke(Stroke::new(1.0, border)) - .rounding(8.0) + .corner_radius(8.0) .inner_margin(12.0) .show(ui, |ui| { ui.label( @@ -1221,10 +1223,10 @@ impl NeuronEncryptApp { .is_some_and(|path| is_vx2_file(path)) { ui.add_space(14.0); - egui::Frame::none() + egui::Frame::new() .fill(Palette::warning_muted()) .stroke(Stroke::new(1.0, Palette::WARNING)) - .rounding(8.0) + .corner_radius(8.0) .inner_margin(12.0) .show(ui, |ui| { ui.label( @@ -1376,10 +1378,10 @@ impl NeuronEncryptApp { ); ui.add_space(12.0); - egui::Frame::none() + egui::Frame::new() .fill(Palette::SURFACE_1) .stroke(Stroke::new(1.0, Palette::BORDER)) - .rounding(8.0) + .corner_radius(8.0) .inner_margin(12.0) .show(ui, |ui| { ui.label( @@ -1899,17 +1901,17 @@ impl eframe::App for NeuronEncryptApp { } egui::CentralPanel::default() - .frame(egui::Frame::none().fill(Palette::BG)) + .frame(egui::Frame::new().fill(Palette::BG)) .show(ctx, |ui| { self.draw_title_bar(ui); ui.add_space(22.0); egui::ScrollArea::vertical().show(ui, |ui| { ui.vertical_centered(|ui| { - egui::Frame::none() + egui::Frame::new() .fill(Palette::SURFACE) .stroke(Stroke::new(1.0, Palette::BORDER)) - .rounding(12.0) + .corner_radius(12.0) .inner_margin(24.0) .show(ui, |ui| { ui.set_width(520.0); diff --git a/neuron-encrypt/src/main.rs b/neuron-encrypt/src/main.rs index aeea358..d4fb564 100644 --- a/neuron-encrypt/src/main.rs +++ b/neuron-encrypt/src/main.rs @@ -60,7 +60,7 @@ fn main() -> eframe::Result<()> { let mut fonts = egui::FontDefinitions::default(); fonts .font_data - .insert("JetBrainsMono".to_owned(), font_data); + .insert("JetBrainsMono".to_owned(), font_data.into()); fonts .families .entry(egui::FontFamily::Monospace)