quick_search/crates/quicksearch-gui/src/app/security_tests.rs

170 lines
6.1 KiB
Rust
Raw Normal View History

use super::*;
use crate::test_ui::{click_at, painted_text, painted_text_center, raw_input};
const SCREEN: egui::Vec2 = egui::vec2(1000.0, 700.0);
/// Two 64-digit keys that differ, in the lowercase form [`IndexKey::to_hex`]
/// produces.
const KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const OTHER: &str = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
/// Two passes: an `egui::Window` is measured on its first frame and placed on
/// the next, so a single pass paints nothing to read back. Same shape as the
/// verify modal's test frame.
fn frame(
ctx: &egui::Context,
display: &str,
events: Vec<egui::Event>,
) -> (egui::FullOutput, (bool, bool)) {
let _ = ctx.run(raw_input(SCREEN, Vec::new()), |ctx| {
reveal_key_modal(ctx, display);
});
let mut buttons = (false, false);
let out = ctx.run(raw_input(SCREEN, events), |ctx| {
buttons = reveal_key_modal(ctx, display);
});
(out, buttons)
}
/// The confirmation half, in the same two passes.
fn confirm_frame(
ctx: &egui::Context,
pw: &mut String,
wrong: bool,
events: Vec<egui::Event>,
) -> (egui::FullOutput, (bool, bool)) {
let _ = ctx.run(raw_input(SCREEN, Vec::new()), |ctx| {
confirm_key_modal(ctx, pw, wrong);
});
let mut buttons = (false, false);
let out = ctx.run(raw_input(SCREEN, events), |ctx| {
buttons = confirm_key_modal(ctx, pw, wrong);
});
(out, buttons)
}
/// An empty field cannot submit: there is nothing to derive from, and a
/// dead button says so more clearly than a rejected attempt would.
#[test]
fn the_confirmation_will_not_submit_an_empty_password() {
let ctx = egui::Context::default();
let mut pw = String::new();
let (out, buttons) = confirm_frame(&ctx, &mut pw, false, Vec::new());
assert_eq!(buttons, (false, false));
let pos = painted_text_center(&out, "Show key").expect("no submit button painted");
let (_, buttons) = confirm_frame(&ctx, &mut pw, false, click_at(pos));
assert!(!buttons.0, "an empty password was submitted");
}
#[test]
fn the_confirmation_submits_a_typed_password_and_cancels_on_request() {
let ctx = egui::Context::default();
let mut pw = "hunter2".to_string();
let (out, _) = confirm_frame(&ctx, &mut pw, false, Vec::new());
assert!(
!painted_text(&out).contains(&pw),
"the password was painted in the clear: {:?}",
painted_text(&out)
);
let submit = painted_text_center(&out, "Show key").expect("no submit button painted");
let (_, buttons) = confirm_frame(&ctx, &mut pw, false, click_at(submit));
assert_eq!(buttons, (true, false));
let cancel = painted_text_center(&out, "Cancel").expect("no cancel button painted");
let (_, buttons) = confirm_frame(&ctx, &mut pw, false, click_at(cancel));
assert_eq!(buttons, (false, true));
}
/// A retry has to say why it is asking again, or it reads as the dialog
/// having ignored the first attempt.
#[test]
fn a_retry_says_the_password_was_wrong() {
let ctx = egui::Context::default();
let mut pw = String::new();
let (quiet, _) = confirm_frame(&ctx, &mut pw, false, Vec::new());
assert!(
!painted_text(&quiet)
.iter()
.any(|t| t.contains("not correct")),
"the first attempt was called wrong before it was made"
);
let (out, _) = confirm_frame(&ctx, &mut pw, true, Vec::new());
assert!(
painted_text(&out)
.iter()
.any(|t| t.contains("That password is not correct")),
"{:?}",
painted_text(&out)
);
}
/// The right password derives the installed key, and the key is shown in the
/// `0x` form other SQLCipher tools take.
#[test]
fn the_matching_password_reveals_the_installed_key() {
assert_eq!(reveal_display(KEY, KEY), Some(format!("0x{KEY}")));
}
/// A wrong password derives some other key. Nothing about the real one may
/// leak from the attempt, so the caller gets no display string at all.
#[test]
fn a_password_that_derives_another_key_reveals_nothing() {
assert_eq!(reveal_display(KEY, OTHER), None);
assert_eq!(reveal_display(KEY, ""), None);
// A prefix must not pass: the whole key is compared, not the start of it.
assert_eq!(reveal_display(KEY, &KEY[..62]), None);
// Both sides come from `to_hex`, which is always lowercase, so an
// uppercase spelling is a mismatch rather than a value to normalise.
assert_eq!(reveal_display(KEY, &KEY.to_uppercase()), None);
}
#[test]
fn the_reveal_shows_the_key_and_what_holding_it_means() {
let ctx = egui::Context::default();
let display = format!("0x{KEY}");
let painted = painted_text(&frame(&ctx, &display, Vec::new()).0);
assert!(
painted.contains(&display),
"the key itself is not on screen: {painted:?}"
);
assert!(
painted
.iter()
.any(|t| t.contains("read the index without the password")),
"no warning about what the key is: {painted:?}"
);
assert!(painted.contains(&"Copy".to_string()), "{painted:?}");
assert!(painted.contains(&"Close".to_string()), "{painted:?}");
}
#[test]
fn both_of_the_reveal_buttons_report_their_click() {
let display = format!("0x{KEY}");
for (label, expected) in [("Copy", (true, false)), ("Close", (false, true))] {
let ctx = egui::Context::default();
let (out, _) = frame(&ctx, &display, Vec::new());
let pos =
painted_text_center(&out, label).unwrap_or_else(|| panic!("no {label} button painted"));
let (_, buttons) = frame(&ctx, &display, click_at(pos));
assert_eq!(
buttons, expected,
"clicking {label} reported the wrong pair"
);
}
}
/// The displayed string is the whole key and nothing else: a truncated or
/// annotated form would be pasted into other tools and fail there.
#[test]
fn the_display_form_is_the_prefix_and_the_whole_key() {
let display = reveal_display(KEY, KEY).expect("a match reveals");
assert_eq!(display.len(), 66);
assert!(display.starts_with("0x"));
assert!(display[2..].bytes().all(|b| b.is_ascii_hexdigit()));
}