feat(gatekeeper): auth flow (setup, count-before-check, threshold/duress wipe, torn-write-safe resume) over Keystore/Random/Prompt ports; host tests
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -290,6 +290,10 @@ version = "0.1.0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gatekeeper"
|
name = "gatekeeper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"keystore",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
keystore = { workspace = true }
|
||||||
|
zeroize = { workspace = true }
|
||||||
|
|||||||
189
crates/servers/gatekeeper/src/flow.rs
Normal file
189
crates/servers/gatekeeper/src/flow.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
//! What the gatekeeper does, over its three ports: the keystore device,
|
||||||
|
//! randomness, and the owner (`Prompt`). No kernel, no hardware: tested on
|
||||||
|
//! the host.
|
||||||
|
|
||||||
|
use keystore::{Block, Copies, KdfParams, Key, Outcome, RECORD_SIZE, Random, Record};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
/// The keystore device: the record's two copies.
|
||||||
|
pub trait Keystore {
|
||||||
|
fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool;
|
||||||
|
fn write(&mut self, copy: usize, data: &[u8; RECORD_SIZE]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A passcode as typed, wiped from memory when dropped.
|
||||||
|
pub struct Secret {
|
||||||
|
bytes: [u8; MAX_SECRET],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MAX_SECRET: usize = 64;
|
||||||
|
|
||||||
|
impl Secret {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { bytes: [0; MAX_SECRET], len: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, typed: &[u8]) {
|
||||||
|
self.bytes.zeroize();
|
||||||
|
let n = typed.len().min(MAX_SECRET);
|
||||||
|
self.bytes[..n].copy_from_slice(&typed[..n]);
|
||||||
|
self.len = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.bytes[..self.len]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same bytes, compared without stopping at the first difference.
|
||||||
|
fn same(&self, other: &Secret) -> bool {
|
||||||
|
let diff = self.bytes.iter().zip(&other.bytes).fold(0u8, |d, (a, b)| d | (a ^ b));
|
||||||
|
(diff | (self.len ^ other.len) as u8) == 0 && self.len == other.len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Secret {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Secret {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.bytes.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The owner, on the screen and keyboard.
|
||||||
|
pub trait Prompt {
|
||||||
|
fn say(&mut self, text: &str);
|
||||||
|
/// Shows `question` and reads a passcode without showing it; false if
|
||||||
|
/// nothing could be read.
|
||||||
|
fn secret(&mut self, question: &str, out: &mut Secret) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why the gatekeeper could not hand over a key.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Failure {
|
||||||
|
/// The keystore could not be read or written.
|
||||||
|
Device,
|
||||||
|
/// No randomness for keys and salts.
|
||||||
|
Random,
|
||||||
|
/// The owner could not be asked.
|
||||||
|
Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrong passcodes in a row that wipe the keys.
|
||||||
|
pub const THRESHOLD: u8 = 10;
|
||||||
|
/// Shortest passcode accepted at setup.
|
||||||
|
pub const MIN_PASSCODE: usize = 4;
|
||||||
|
|
||||||
|
/// The keystore's newest record, and where the next write goes.
|
||||||
|
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> {
|
||||||
|
let (mut a, mut b) = ([0u8; RECORD_SIZE], [0u8; RECORD_SIZE]);
|
||||||
|
if !ks.read(0, &mut a) || !ks.read(1, &mut b) {
|
||||||
|
return Err(Failure::Device);
|
||||||
|
}
|
||||||
|
let copies = Copies { copies: [&a, &b] };
|
||||||
|
Ok((copies.newest().map(|(r, _)| r), copies.next_copy()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(ks: &mut impl Keystore, copy: usize, r: &Record) -> Result<(), Failure> {
|
||||||
|
if ks.write(copy, &r.encode()) { Ok(()) } else { Err(Failure::Device) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroys the key slots: noise over both copies, twice, then blank (the
|
||||||
|
/// device then looks never set up). Marked as wiping first, so a power cut
|
||||||
|
/// part way is finished on the next boot.
|
||||||
|
fn wipe(ks: &mut impl Keystore, record: &Record, next: usize, rng: &mut impl Random) -> Result<(), Failure> {
|
||||||
|
let first = record.wiping(rng).ok_or(Failure::Random)?;
|
||||||
|
let second = first.wiping(rng).ok_or(Failure::Random)?;
|
||||||
|
for r in [&first, &second] {
|
||||||
|
store(ks, next, r)?;
|
||||||
|
store(ks, 1 - next, r)?;
|
||||||
|
}
|
||||||
|
let blank = [0u8; RECORD_SIZE];
|
||||||
|
if ks.write(next, &blank) && ks.write(1 - next, &blank) { Ok(()) } else { Err(Failure::Device) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks for a new passcode twice until both match; `optional` allows none.
|
||||||
|
fn choose(prompt: &mut impl Prompt, question: &str, optional: bool, out: &mut Secret) -> Result<(), Failure> {
|
||||||
|
loop {
|
||||||
|
let mut again = Secret::new();
|
||||||
|
if !prompt.secret(question, out) || !prompt.secret("Again: ", &mut again) {
|
||||||
|
return Err(Failure::Prompt);
|
||||||
|
}
|
||||||
|
if !out.same(&again) {
|
||||||
|
prompt.say("They differ; once more.\n");
|
||||||
|
} else if out.len == 0 && optional {
|
||||||
|
return Ok(());
|
||||||
|
} else if out.len < MIN_PASSCODE {
|
||||||
|
prompt.say("At least 4 characters.\n");
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device never set up (or wiped): the owner chooses a passcode, and
|
||||||
|
/// may choose a duress one. Returns the new master key.
|
||||||
|
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
|
||||||
|
prompt.say("This device has no passcode yet. Choose one; it protects everything on it.\n");
|
||||||
|
let mut real = Secret::new();
|
||||||
|
choose(prompt, "New passcode: ", false, &mut real)?;
|
||||||
|
prompt.say("A duress passcode wipes the device when it is entered instead. Leave it empty for none.\n");
|
||||||
|
let mut duress = Secret::new();
|
||||||
|
loop {
|
||||||
|
choose(prompt, "Duress passcode: ", true, &mut duress)?;
|
||||||
|
if duress.len == 0 || !duress.same(&real) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prompt.say("It must differ from the passcode.\n");
|
||||||
|
}
|
||||||
|
let duress = (duress.len > 0).then_some(duress.as_bytes());
|
||||||
|
let (record, key) = Record::setup(real.as_bytes(), duress, THRESHOLD, params, memory, rng).ok_or(Failure::Random)?;
|
||||||
|
store(ks, 0, &record)?;
|
||||||
|
store(ks, 1, &record)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticates the owner and returns the master key: setting one up on
|
||||||
|
/// a fresh device, wiping on duress or too many failures (the device then
|
||||||
|
/// looks fresh, and is set up anew).
|
||||||
|
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
|
||||||
|
loop {
|
||||||
|
let (record, next) = load(ks)?;
|
||||||
|
let Some(record) = record else {
|
||||||
|
return setup(ks, rng, prompt, params, memory);
|
||||||
|
};
|
||||||
|
if record.must_wipe() {
|
||||||
|
wipe(ks, &record, next, rng)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut typed = Secret::new();
|
||||||
|
if !prompt.secret("Passcode: ", &mut typed) {
|
||||||
|
return Err(Failure::Prompt);
|
||||||
|
}
|
||||||
|
// Counted before it is checked: pulling the power cannot save a guess.
|
||||||
|
let counted = record.next(|r| r.failures = r.failures.saturating_add(1));
|
||||||
|
store(ks, next, &counted)?;
|
||||||
|
match record.unlock(typed.as_bytes(), memory) {
|
||||||
|
Outcome::Real(key) => {
|
||||||
|
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
|
||||||
|
return Ok(key);
|
||||||
|
}
|
||||||
|
Outcome::Duress => {
|
||||||
|
// Recorded as worn out first: a wipe cut short resumes.
|
||||||
|
store(ks, 1 - next, &counted.next(|r| r.failures = r.threshold))?;
|
||||||
|
let (worn, next) = load(ks)?;
|
||||||
|
wipe(ks, &worn.ok_or(Failure::Device)?, next, rng)?;
|
||||||
|
}
|
||||||
|
Outcome::Wrong => {
|
||||||
|
let left = counted.threshold.saturating_sub(counted.failures);
|
||||||
|
if left > 0 {
|
||||||
|
prompt.say(if left == 1 { "Wrong passcode. One try left before the device is wiped.\n" } else { "Wrong passcode.\n" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
|
//! The gatekeeper (see the vision's Gatekeeper note): authenticates the
|
||||||
|
//! owner before anything that touches user data starts, and hands the
|
||||||
|
//! master key over. `flow` is the logic; `run` wires the devices.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
pub fn add(left: u64, right: u64) -> u64 {
|
extern crate alloc;
|
||||||
left + right
|
|
||||||
}
|
pub mod flow;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "tests/mod.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
212
crates/servers/gatekeeper/src/tests/mod.rs
Normal file
212
crates/servers/gatekeeper/src/tests/mod.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
extern crate std;
|
||||||
|
|
||||||
|
use crate::flow::*;
|
||||||
|
use keystore::{Block, KdfParams, RECORD_SIZE, Random, Record};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::string::String;
|
||||||
|
use std::vec;
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
|
const FAST: KdfParams = KdfParams { memory_kib: 8, passes: 1 };
|
||||||
|
|
||||||
|
/// The keystore in memory; `cut_after` writes simulate the power going.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Device {
|
||||||
|
copies: [Vec<u8>; 2],
|
||||||
|
writes: usize,
|
||||||
|
cut_after: Option<usize>,
|
||||||
|
/// The power goes in the middle of the first write that starts a
|
||||||
|
/// wipe: only its start lands.
|
||||||
|
tear_wipe: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { copies: [vec![0; RECORD_SIZE], vec![0; RECORD_SIZE]], ..Self::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record(&self) -> Option<Record> {
|
||||||
|
keystore::Copies { copies: [&self.copies[0], &self.copies[1]] }.newest().map(|(r, _)| r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keystore for Device {
|
||||||
|
fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool {
|
||||||
|
out.copy_from_slice(&self.copies[copy]);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn write(&mut self, copy: usize, data: &[u8; RECORD_SIZE]) -> bool {
|
||||||
|
if self.cut_after.is_some_and(|n| self.writes >= n) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let starts_wipe = Record::decode(data).is_some_and(|r| r.state == keystore::State::Wiping);
|
||||||
|
if self.tear_wipe && starts_wipe {
|
||||||
|
// Into the slots: the record (and its checksum) is not whole.
|
||||||
|
self.copies[copy][..100].copy_from_slice(&data[..100]);
|
||||||
|
self.cut_after = Some(self.writes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.writes += 1;
|
||||||
|
self.copies[copy] = data.to_vec();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Counter(u8);
|
||||||
|
|
||||||
|
impl Random for Counter {
|
||||||
|
fn fill(&mut self, buf: &mut [u8]) -> bool {
|
||||||
|
for b in buf {
|
||||||
|
self.0 = self.0.wrapping_mul(37).wrapping_add(11);
|
||||||
|
*b = self.0;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An owner typing from a script; what was shown is kept.
|
||||||
|
struct Owner {
|
||||||
|
typing: VecDeque<&'static str>,
|
||||||
|
shown: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Owner {
|
||||||
|
fn types(lines: &[&'static str]) -> Self {
|
||||||
|
Self { typing: lines.iter().copied().collect(), shown: String::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Prompt for Owner {
|
||||||
|
fn say(&mut self, text: &str) {
|
||||||
|
self.shown.push_str(text);
|
||||||
|
}
|
||||||
|
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||||
|
self.shown.push_str(question);
|
||||||
|
let Some(line) = self.typing.pop_front() else { return false };
|
||||||
|
out.set(line.as_bytes());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device set up with passcode 1234 and 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fresh_device_is_set_up_and_then_unlocks_with_its_passcode() {
|
||||||
|
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");
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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!(dev.record().unwrap().failures, 1, "the guess stays counted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_passcodes_are_counted_and_a_right_one_resets_the_count() {
|
||||||
|
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();
|
||||||
|
assert!(owner.shown.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_duress_passcode_wipes_silently_and_the_device_looks_fresh() {
|
||||||
|
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.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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
dev.cut_after = None;
|
||||||
|
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user