feat(users): keystore v2 (8 slots, per-user salt+name tag, owner admin key opens sealed user dir, per-user duress); gatekeeper login by name+passcode, owner menu (add/remove/guests), passcode change, guest sessions

This commit is contained in:
2026-10-28 00:29:07 +01:00
parent 9dc751c466
commit 29a25bd8c2
9 changed files with 1232 additions and 417 deletions

View File

@@ -1,7 +1,7 @@
extern crate std;
use crate::flow::*;
use keystore::{Block, KdfParams, RECORD_SIZE, Random, Record};
use keystore::{Block, KdfParams, RECORD_SIZE, Random, Record, Role};
use std::collections::VecDeque;
use std::string::String;
use std::vec;
@@ -90,144 +90,160 @@ impl Prompt for Owner {
}
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
self.shown.push_str(question);
self.said.push_str(question);
let Some(line) = self.typing.pop_front() else { return false };
out.set(line.as_bytes());
true
}
fn line(&mut self, question: &str, out: &mut Secret) -> bool {
self.secret(question, out)
}
}
fn memory() -> Vec<Block> {
vec![Block::default(); FAST.memory_kib as usize]
}
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<keystore::Key, Failure> {
run(dev, &mut Counter(5), owner, FAST, &mut memory()).map(|u| u.key)
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<Unlocked, Failure> {
run(dev, &mut Counter(5), owner, FAST, &mut memory())
}
fn fresh(dev: &mut Device, owner: &mut Owner) -> bool {
run(dev, &mut Counter(5), owner, FAST, &mut memory()).unwrap().fresh
fn key_of(dev: &mut Device, lines: &[&'static str]) -> Option<[u8; 32]> {
run_with(dev, &mut Owner::types(lines)).ok().map(|u| u.key.0)
}
/// A device set up with passcode 1234 and duress 0000; its master key.
/// Set up with owner gabriel, passcode 1234, duress 0000; its master key.
fn set_up() -> (Device, [u8; 32]) {
let mut dev = Device::new();
let key = run_with(&mut dev, &mut Owner::types(&["1234", "1234", "0000", "0000"])).unwrap();
(dev, key.0)
let u = run_with(&mut dev, &mut Owner::types(&["gabriel", "1234", "1234", "0000", "0000"])).unwrap();
assert!(u.fresh && u.user.unwrap().role == Role::Owner);
(dev, u.key.0)
}
/// Signs gabriel in and continues past the menu.
const SIGN_IN: [&str; 3] = ["gabriel", "1234", ""];
#[test]
fn a_fresh_device_is_set_up_and_then_unlocks_with_its_passcode() {
fn the_owner_signs_in_with_name_and_passcode_each_boot() {
let (mut dev, key) = set_up();
let mut owner = Owner::types(&["1234"]);
assert_eq!(run_with(&mut dev, &mut owner).unwrap().0, key, "the same master key every boot");
let mut owner = Owner::types(&SIGN_IN);
let u = run_with(&mut dev, &mut owner).unwrap();
assert_eq!(u.key.0, key);
assert!(!u.fresh);
assert_eq!(u.user.unwrap().name.as_bytes(), b"gabriel");
assert!(owner.said.contains("[u] users"), "an owner is offered user management");
assert_eq!(dev.record().unwrap().failures, 0);
}
#[test]
fn setup_insists_on_a_confirmed_passcode_of_four_or_more() {
let mut dev = Device::new();
let mut owner = Owner::types(&["1234", "1243", "12", "12", "123456", "123456", "", ""]);
run_with(&mut dev, &mut owner).unwrap();
assert!(owner.shown.contains("They differ"));
assert!(owner.shown.contains("At least 4"));
let mut unlock = Owner::types(&["123456"]);
assert!(run_with(&mut dev, &mut unlock).is_ok(), "no duress chosen: the passcode is 123456");
}
#[test]
fn the_duress_passcode_must_differ() {
let mut dev = Device::new();
let mut owner = Owner::types(&["1234", "1234", "1234", "1234", "", ""]);
run_with(&mut dev, &mut owner).unwrap();
assert!(owner.shown.contains("must differ"));
fn a_wrong_name_and_a_wrong_passcode_look_alike_and_both_count() {
let (mut dev, key) = set_up();
let mut owner = Owner::types(&["nobody", "1234", "gabriel", "9999", "gabriel", "1234", ""]);
assert_eq!(run_with(&mut dev, &mut owner).unwrap().key.0, key);
assert_eq!(owner.said.matches("Wrong name or passcode.").count(), 2);
}
#[test]
fn every_guess_is_counted_before_it_is_checked() {
let (mut dev, _) = set_up();
// The power goes after the guess was counted, before the check could
// clear it: even the right passcode stays counted.
dev.cut_after = Some(dev.writes + 1);
let mut owner = Owner::types(&["1234"]);
assert_eq!(run_with(&mut dev, &mut owner).err(), Some(Failure::Device));
assert_eq!(run_with(&mut dev, &mut Owner::types(&SIGN_IN)).err(), Some(Failure::Device));
assert_eq!(dev.record().unwrap().failures, 1, "the guess stays counted");
}
#[test]
fn wrong_passcodes_are_counted_and_a_right_one_resets_the_count() {
fn too_many_wrong_passcodes_wipe_everyone_and_the_device_starts_fresh() {
let (mut dev, key) = set_up();
let mut owner = Owner::types(&["1111", "2222", "1234"]);
assert_eq!(run_with(&mut dev, &mut owner).unwrap().0, key);
assert_eq!(owner.shown.matches("Wrong passcode").count(), 2);
assert_eq!(dev.record().unwrap().failures, 0);
}
#[test]
fn too_many_wrong_passcodes_wipe_the_keys_and_the_device_starts_fresh() {
let (mut dev, key) = set_up();
let mut guesses = vec!["9999"; THRESHOLD as usize];
// Then it looks fresh: the owner sets it up again.
guesses.extend(["5678", "5678", "", ""]);
let mut owner = Owner::types(&guesses);
let new = run_with(&mut dev, &mut owner).unwrap();
let mut lines: Vec<&'static str> = Vec::new();
for _ in 0..THRESHOLD {
lines.extend(["gabriel", "9999"]);
}
lines.extend(["anna", "5678", "5678", "", ""]);
let mut owner = Owner::types(&lines);
let u = run_with(&mut dev, &mut owner).unwrap();
assert!(owner.said.contains("One try left"));
assert!(owner.shown.ends_with("This device has no passcode yet. Choose one; it protects everything on it.\nNew passcode: Again: A duress passcode wipes the device when it is entered instead. Leave it empty for none.\nDuress passcode: Again: "));
assert_ne!(new.0, key, "the old master key is gone");
let mut old = Owner::types(&["1234"]);
let mut dev2 = dev.clone();
dev2.cut_after = None;
assert!(run_with(&mut dev2, &mut old).is_err(), "the old passcode opens nothing");
assert!(u.fresh);
assert_ne!(u.key.0, key);
assert!(owner.shown.starts_with("This device has no users yet"), "nothing earlier left on the screen");
}
#[test]
fn the_duress_passcode_wipes_silently_and_the_device_looks_fresh() {
fn the_duress_passcode_wipes_silently() {
let (mut dev, key) = set_up();
let mut owner = Owner::types(&["0000", "4321", "4321", "", ""]);
let new = run_with(&mut dev, &mut owner).unwrap();
assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode");
assert!(owner.shown.starts_with("This device has no passcode yet"), "nor is the passcode prompt left showing");
assert!(owner.shown.contains("no passcode yet"));
assert_ne!(new.0, key);
}
#[test]
fn a_wipe_cut_short_is_finished_on_the_next_boot() {
let (mut dev, key) = set_up();
// Duress, and the power goes during the wipe (after its first write).
let marked = dev.writes + 2;
dev.cut_after = Some(marked + 1);
assert_eq!(run_with(&mut dev, &mut Owner::types(&["0000"])).err(), Some(Failure::Device));
dev.cut_after = None;
// Next boot: the wipe completes before anything is asked; then setup.
let mut owner = Owner::types(&["4321", "4321", "", ""]);
let new = run_with(&mut dev, &mut owner).unwrap();
assert!(owner.shown.starts_with("This device has no passcode yet"), "no passcode is asked of a half-wiped device");
assert_ne!(new.0, key);
}
#[test]
fn a_duress_cut_short_before_the_wipe_still_wipes_next_boot() {
let (mut dev, _) = set_up();
// The power goes right after the duress was recorded (before any wipe write).
dev.cut_after = Some(dev.writes + 2);
assert_eq!(run_with(&mut dev, &mut Owner::types(&["0000"])).err(), Some(Failure::Device));
assert!(dev.record().unwrap().must_wipe());
let mut owner = Owner::types(&["gabriel", "0000", "anna", "4321", "4321", "", ""]);
let u = run_with(&mut dev, &mut owner).unwrap();
assert!(!owner.said.contains("Wrong"));
assert!(owner.shown.starts_with("This device has no users yet"));
assert!(u.fresh);
assert_ne!(u.key.0, key);
}
#[test]
fn a_duress_whose_wipe_is_torn_still_wipes_next_boot() {
let (mut dev, _) = set_up();
// Counted, recorded, then the wipe's first write is torn.
dev.tear_wipe = true;
assert_eq!(run_with(&mut dev, &mut Owner::types(&["0000"])).err(), Some(Failure::Device));
assert_eq!(run_with(&mut dev, &mut Owner::types(&["gabriel", "0000"])).err(), Some(Failure::Device));
dev.cut_after = None;
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
}
#[test]
fn only_a_new_master_key_counts_as_fresh() {
let mut dev = Device::new();
assert!(fresh(&mut dev, &mut Owner::types(&["1234", "1234", "0000", "0000"])), "first boot");
assert!(!fresh(&mut dev, &mut Owner::types(&["1234"])), "an unlock");
assert!(fresh(&mut dev, &mut Owner::types(&["0000", "5678", "5678", "", ""])), "after a duress wipe");
fn a_wipe_cut_short_is_finished_on_the_next_boot() {
let (mut dev, key) = set_up();
dev.cut_after = Some(dev.writes + 3);
assert_eq!(run_with(&mut dev, &mut Owner::types(&["gabriel", "0000"])).err(), Some(Failure::Device));
dev.cut_after = None;
let mut owner = Owner::types(&["anna", "4321", "4321", "", ""]);
let u = run_with(&mut dev, &mut owner).unwrap();
assert!(owner.shown.starts_with("This device has no users yet"));
assert_ne!(u.key.0, key);
}
#[test]
fn an_owner_adds_a_user_who_then_has_their_own_key() {
let (mut dev, key) = set_up();
// Sign in, users, add work (not an owner), passcode 5678 twice, no duress, done, continue.
let added = key_of(&mut dev, &["gabriel", "1234", "u", "a", "work", "n", "5678", "5678", "", "", "", ""]).unwrap();
assert_eq!(added, key, "the owner's own session");
// `u` first: it must do nothing for a standard user.
let mut work = Owner::types(&["work", "5678", "u", ""]);
let u = run_with(&mut dev, &mut work).unwrap();
assert_eq!(u.user.unwrap().role, Role::Standard);
assert_ne!(u.key.0, key);
assert!(!work.said.contains("[u] users"), "no user management for a standard user");
assert!(!work.said.contains("Users:"), "not even when asked for");
}
#[test]
fn removed_users_are_reported_and_shut_out() {
let (mut dev, _) = set_up();
key_of(&mut dev, &["gabriel", "1234", "u", "a", "work", "n", "5678", "5678", "", "", "", ""]).unwrap();
let u = run_with(&mut dev, &mut Owner::types(&["gabriel", "1234", "u", "r", "work", "", ""])).unwrap();
assert_eq!(u.removed.iter().flatten().count(), 1, "work's files are to go");
let mut work = Owner::types(&["work", "5678"]);
assert!(run_with(&mut dev, &mut work).is_err());
assert!(work.said.contains("Wrong name or passcode"));
}
#[test]
fn a_user_changes_their_passcode() {
let (mut dev, key) = set_up();
key_of(&mut dev, &["gabriel", "1234", "p", "9876", "9876", ""]).unwrap();
assert_eq!(key_of(&mut dev, &["gabriel", "9876", ""]), Some(key), "the new one opens the same key");
let mut old = Owner::types(&["gabriel", "1234"]);
assert!(run_with(&mut dev, &mut old).is_err());
}
#[test]
fn guests_come_in_only_when_allowed_and_get_a_throwaway_key() {
let (mut dev, _) = set_up();
let mut nope = Owner::types(&[""]);
assert!(run_with(&mut dev, &mut nope).is_err());
assert!(nope.said.contains("Guests are not allowed"));
key_of(&mut dev, &["gabriel", "1234", "u", "g", "", ""]).unwrap();
// (Two sessions, two draws from the hardware.)
let a = run(&mut dev, &mut Counter(1), &mut Owner::types(&[""]), FAST, &mut memory()).unwrap();
let b = run(&mut dev, &mut Counter(2), &mut Owner::types(&[""]), FAST, &mut memory()).unwrap();
assert!(a.user.is_none());
assert_ne!(a.key.0, b.key.0, "each guest session's key is new");
}