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:
@@ -9,16 +9,27 @@ use types::bounded::CapSlot;
|
||||
use crate::flow::{Keystore, MAX_SECRET, Prompt, Secret};
|
||||
use keystore::RECORD_SIZE;
|
||||
|
||||
/// The record's two copies are its first two sectors.
|
||||
/// The record's two copies, one after the other from the first sector.
|
||||
pub struct Disk<D: BlockDevice>(pub D);
|
||||
|
||||
impl<D: BlockDevice> Disk<D> {
|
||||
/// Sector `i` of copy `copy`, for each of the sectors a record takes.
|
||||
fn sectors(&self, copy: usize) -> impl Iterator<Item = (usize, SectorId)> {
|
||||
let size = self.0.sector_size().max(1);
|
||||
let per = RECORD_SIZE / size;
|
||||
(0..per).map(move |i| (i * size, SectorId::new((copy * per + i) as u32)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: BlockDevice> Keystore for Disk<D> {
|
||||
fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool {
|
||||
self.0.sector_size() == RECORD_SIZE && self.0.read_sector(SectorId::new(copy as u32), out)
|
||||
let size = self.0.sector_size();
|
||||
RECORD_SIZE.is_multiple_of(size) && self.sectors(copy).collect::<alloc::vec::Vec<_>>().into_iter().all(|(at, s)| self.0.read_sector(s, &mut out[at..at + size]))
|
||||
}
|
||||
|
||||
fn write(&mut self, copy: usize, data: &[u8; RECORD_SIZE]) -> bool {
|
||||
self.0.sector_size() == RECORD_SIZE && self.0.write_sector(SectorId::new(copy as u32), data)
|
||||
let size = self.0.sector_size();
|
||||
RECORD_SIZE.is_multiple_of(size) && self.sectors(copy).collect::<alloc::vec::Vec<_>>().into_iter().all(|(at, s)| self.0.write_sector(s, &data[at..at + size]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +52,14 @@ impl Prompt for Terminal {
|
||||
libkos::terminal::clear(self.0);
|
||||
}
|
||||
|
||||
fn line(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||
print_to(self.0, format_args!("{}", question));
|
||||
let mut buf = [0u8; MAX_SECRET + 1];
|
||||
let n = libkos::terminal::read_line(self.0, &mut buf);
|
||||
out.set(buf[..n].strip_suffix(b"\n").unwrap_or(&buf[..n]));
|
||||
n > 0
|
||||
}
|
||||
|
||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||
print_to(self.0, format_args!("{}", question));
|
||||
let mut line = Secret::new();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! 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 keystore::{Block, Copies, KdfParams, Key, Name, Outcome, RECORD_SIZE, Random, Record, Refused, Role, SLOTS, Session, User, UserId};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// The keystore device: the record's two copies.
|
||||
@@ -62,6 +62,8 @@ pub trait Prompt {
|
||||
/// 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;
|
||||
/// Shows `question` and reads an ordinary line (a name, a choice).
|
||||
fn line(&mut self, question: &str, out: &mut Secret) -> bool;
|
||||
}
|
||||
|
||||
/// Why the gatekeeper could not hand over a key.
|
||||
@@ -75,11 +77,23 @@ pub enum Failure {
|
||||
Prompt,
|
||||
}
|
||||
|
||||
/// Wrong passcodes in a row that wipe the keys.
|
||||
/// Wrong passcodes in a row (for any name) that wipe every user.
|
||||
pub const THRESHOLD: u8 = 10;
|
||||
/// Shortest passcode accepted at setup.
|
||||
/// Shortest passcode accepted.
|
||||
pub const MIN_PASSCODE: usize = 4;
|
||||
|
||||
/// Someone is in.
|
||||
pub struct Unlocked {
|
||||
/// Who, or `None` for a guest (whose key is made up for this session).
|
||||
pub user: Option<User>,
|
||||
pub key: Key,
|
||||
/// The keystore is new (first boot, or just wiped): nothing sealed
|
||||
/// under an earlier key exists any more.
|
||||
pub fresh: bool,
|
||||
/// Users removed just now, whose files are to go.
|
||||
pub removed: [Option<UserId>; SLOTS],
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
@@ -94,9 +108,15 @@ 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.
|
||||
/// `r` written as the newest record.
|
||||
fn save(ks: &mut impl Keystore, r: &Record) -> Result<(), Failure> {
|
||||
let (_, next) = load(ks)?;
|
||||
store(ks, next, r)
|
||||
}
|
||||
|
||||
/// Destroys every slot and the directory: 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)?;
|
||||
@@ -127,50 +147,132 @@ fn choose(prompt: &mut impl Prompt, question: &str, optional: bool, out: &mut Se
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// After a wipe nothing earlier may show: it must look like a new device.
|
||||
prompt.clear();
|
||||
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();
|
||||
/// A name, asked until it is a valid one.
|
||||
fn ask_name(prompt: &mut impl Prompt, question: &str) -> Result<Name, Failure> {
|
||||
loop {
|
||||
choose(prompt, "Duress passcode: ", true, &mut duress)?;
|
||||
if duress.len == 0 || !duress.same(&real) {
|
||||
break;
|
||||
let mut typed = Secret::new();
|
||||
if !prompt.line(question, &mut typed) {
|
||||
return Err(Failure::Prompt);
|
||||
}
|
||||
match Name::new(typed.as_bytes()) {
|
||||
Some(n) => return Ok(n),
|
||||
None => prompt.say("A name is 1 to 16 letters, digits or signs, no spaces.\n"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A passcode and an optional duress passcode, different from it.
|
||||
fn ask_passcodes(prompt: &mut impl Prompt, real: &mut Secret, duress: &mut Secret) -> Result<(), Failure> {
|
||||
choose(prompt, "New passcode: ", false, real)?;
|
||||
prompt.say("A duress passcode wipes the device when it is entered instead. Leave it empty for none.\n");
|
||||
loop {
|
||||
choose(prompt, "Duress passcode: ", true, duress)?;
|
||||
if duress.len == 0 || !duress.same(real) {
|
||||
return Ok(());
|
||||
}
|
||||
prompt.say("It must differ from the passcode.\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// A device never set up (or wiped): its first user, an owner.
|
||||
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
|
||||
// After a wipe nothing earlier may show: it must look like a new device.
|
||||
prompt.clear();
|
||||
prompt.say("This device has no users yet. Set up its owner; the passcode protects everything on it.\n");
|
||||
let name = ask_name(prompt, "Your name: ")?;
|
||||
let (mut real, mut duress) = (Secret::new(), Secret::new());
|
||||
ask_passcodes(prompt, &mut real, &mut duress)?;
|
||||
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)?;
|
||||
let (record, session) = Record::setup(name, real.as_bytes(), duress, THRESHOLD, params, memory, rng).ok_or(Failure::Random)?;
|
||||
store(ks, 0, &record)?;
|
||||
store(ks, 1, &record)?;
|
||||
Ok(key)
|
||||
Ok(Unlocked { user: Some(session.user), key: session.key.clone(), fresh: true, removed: [None; SLOTS] })
|
||||
}
|
||||
|
||||
/// The owner is in: the master key, and whether it is new (a fresh
|
||||
/// device, or one just wiped), so nothing sealed under an old one exists.
|
||||
pub struct Unlocked {
|
||||
pub key: Key,
|
||||
pub fresh: bool,
|
||||
/// What the owner may change at the lock screen, after signing in.
|
||||
fn manage(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, memory: &mut [Block], session: &Session, removed: &mut [Option<UserId>; SLOTS]) -> Result<(), Failure> {
|
||||
loop {
|
||||
let (Some(record), _) = load(ks)? else { return Err(Failure::Device) };
|
||||
let users = record.users(session).map_err(|_| Failure::Device)?;
|
||||
prompt.say("Users:");
|
||||
for u in users.iter().flatten() {
|
||||
prompt.say(" ");
|
||||
prompt.say(core::str::from_utf8(u.name.as_bytes()).unwrap_or("?"));
|
||||
if u.role == Role::Owner {
|
||||
prompt.say(" (owner)");
|
||||
}
|
||||
}
|
||||
prompt.say(if record.guests { ". Guests: allowed.\n" } else { ". Guests: not allowed.\n" });
|
||||
let mut choice = Secret::new();
|
||||
if !prompt.line("[a] add, [r] remove, [g] guests on/off, [Enter] done: ", &mut choice) {
|
||||
return Err(Failure::Prompt);
|
||||
}
|
||||
let changed = match choice.as_bytes() {
|
||||
b"" => return Ok(()),
|
||||
b"g" => Ok(record.next(|r| r.guests = !r.guests)),
|
||||
b"a" => {
|
||||
let name = ask_name(prompt, "Name: ")?;
|
||||
let mut role = Secret::new();
|
||||
if !prompt.line("Owner too? [y/N] ", &mut role) {
|
||||
return Err(Failure::Prompt);
|
||||
}
|
||||
let role = if role.as_bytes() == b"y" { Role::Owner } else { Role::Standard };
|
||||
let (mut real, mut duress) = (Secret::new(), Secret::new());
|
||||
ask_passcodes(prompt, &mut real, &mut duress)?;
|
||||
let duress = (duress.len > 0).then_some(duress.as_bytes());
|
||||
record.add_user(session, name, role, real.as_bytes(), duress, memory, rng).map(|(r, _)| r)
|
||||
}
|
||||
b"r" => {
|
||||
let name = ask_name(prompt, "Remove whom: ")?;
|
||||
record.remove_user(session, &name, rng).map(|(r, id)| {
|
||||
if let Some(slot) = removed.iter_mut().find(|s| s.is_none()) {
|
||||
*slot = Some(id);
|
||||
}
|
||||
r
|
||||
})
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
match changed {
|
||||
Ok(r) => save(ks, &r)?,
|
||||
Err(Refused::NameTaken) => prompt.say("That name is taken.\n"),
|
||||
Err(Refused::Full) => prompt.say("No room for another user.\n"),
|
||||
Err(Refused::NoSuchUser) => prompt.say("No such user.\n"),
|
||||
Err(Refused::LastOwner) => prompt.say("The device needs an owner.\n"),
|
||||
Err(_) => return Err(Failure::Random),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// Authenticates someone and returns their key: setting up a fresh
|
||||
/// device's owner, wiping everyone on a duress passcode 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<Unlocked, Failure> {
|
||||
loop {
|
||||
let (record, next) = load(ks)?;
|
||||
let Some(record) = record else {
|
||||
return setup(ks, rng, prompt, params, memory).map(|key| Unlocked { key, fresh: true });
|
||||
return setup(ks, rng, prompt, params, memory);
|
||||
};
|
||||
if record.must_wipe() {
|
||||
wipe(ks, &record, next, rng)?;
|
||||
continue;
|
||||
}
|
||||
let mut name = Secret::new();
|
||||
if !prompt.line("Name: ", &mut name) {
|
||||
return Err(Failure::Prompt);
|
||||
}
|
||||
if name.len == 0 {
|
||||
if !record.guests {
|
||||
prompt.say("Guests are not allowed here.\n");
|
||||
continue;
|
||||
}
|
||||
// A key nobody keeps: what a guest leaves behind is noise.
|
||||
let mut key = Key([0; keystore::KEY_SIZE]);
|
||||
if !rng.fill(&mut key.0) {
|
||||
return Err(Failure::Random);
|
||||
}
|
||||
return Ok(Unlocked { user: None, key, fresh: false, removed: [None; SLOTS] });
|
||||
}
|
||||
let mut typed = Secret::new();
|
||||
if !prompt.secret("Passcode: ", &mut typed) {
|
||||
return Err(Failure::Prompt);
|
||||
@@ -178,10 +280,31 @@ pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prom
|
||||
// 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) => {
|
||||
match record.unlock(name.as_bytes(), typed.as_bytes(), memory) {
|
||||
Outcome::User(session) => {
|
||||
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
|
||||
return Ok(Unlocked { key, fresh: false });
|
||||
let mut removed = [None; SLOTS];
|
||||
loop {
|
||||
let mut choice = Secret::new();
|
||||
let menu = if session.is_owner() { "[Enter] continue, [p] passcode, [u] users: " } else { "[Enter] continue, [p] passcode: " };
|
||||
if !prompt.line(menu, &mut choice) {
|
||||
return Err(Failure::Prompt);
|
||||
}
|
||||
match choice.as_bytes() {
|
||||
b"" => break,
|
||||
b"p" => {
|
||||
let mut new = Secret::new();
|
||||
choose(prompt, "New passcode: ", false, &mut new)?;
|
||||
let (Some(now), _) = load(ks)? else { return Err(Failure::Device) };
|
||||
let changed = now.change_passcode(&session, new.as_bytes(), memory, rng).map_err(|_| Failure::Random)?;
|
||||
save(ks, &changed)?;
|
||||
prompt.say("Passcode changed.\n");
|
||||
}
|
||||
b"u" if session.is_owner() => manage(ks, rng, prompt, memory, &session, &mut removed)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Ok(Unlocked { user: Some(session.user), key: session.key.clone(), fresh: false, removed });
|
||||
}
|
||||
Outcome::Duress => {
|
||||
// Recorded as worn out first: a wipe cut short resumes.
|
||||
@@ -192,7 +315,7 @@ pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prom
|
||||
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" });
|
||||
prompt.say(if left == 1 { "Wrong name or passcode. One try left before the device is wiped.\n" } else { "Wrong name or passcode.\n" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use alloc::vec;
|
||||
use keystore::{Block, KdfParams};
|
||||
use libkos::syscall::{self, Received};
|
||||
use types::bounded::CapSlot;
|
||||
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
|
||||
use types::gatekeeper::{KEY, NAME, REMOVED, SEED, UNLOCK, reply, unlocked};
|
||||
|
||||
/// Where init's manifest put them: its own endpoint (devices arrive on
|
||||
/// it, then init's calls), then the terminal.
|
||||
@@ -67,18 +67,48 @@ pub fn run() -> ! {
|
||||
}
|
||||
drop(memory);
|
||||
match unlocked {
|
||||
Ok(flow::Unlocked { key, fresh }) => {
|
||||
Ok(flow::Unlocked { user, key, fresh, removed }) => {
|
||||
libkos::println!("gatekeeper: unlocked");
|
||||
libkos::terminal::clear(TERMINAL);
|
||||
let words = |b: &[u8; 32]| core::array::from_fn::<usize, 4, _>(|i| usize::from_le_bytes(b[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])));
|
||||
let mut seed = [0u8; 32];
|
||||
let seeded = keystore::Random::fill(&mut random, &mut seed);
|
||||
if seeded && answer(UNLOCK, [reply::OK, usize::from(fresh), 0, 0]) && answer(KEY, words(&key.0)) {
|
||||
let _ = answer(SEED, words(&seed));
|
||||
} else if !seeded {
|
||||
if !keystore::Random::fill(&mut random, &mut seed) {
|
||||
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
|
||||
syscall::exit();
|
||||
}
|
||||
let flags = if fresh { unlocked::FRESH } else { 0 } | if user.is_none() { unlocked::GUEST } else { 0 };
|
||||
let id = user.map_or(0, |u| usize::from_le_bytes(u.id.0));
|
||||
let role = match user.map(|u| u.role) {
|
||||
Some(keystore::Role::Owner) => unlocked::OWNER,
|
||||
Some(keystore::Role::Standard) => unlocked::STANDARD,
|
||||
None => 0,
|
||||
};
|
||||
let mut name_buf = [0u8; 16];
|
||||
let len = match &user {
|
||||
Some(u) => {
|
||||
let n = u.name.as_bytes();
|
||||
name_buf[..n.len()].copy_from_slice(n);
|
||||
n.len()
|
||||
}
|
||||
None => {
|
||||
name_buf[..5].copy_from_slice(b"guest");
|
||||
5
|
||||
}
|
||||
};
|
||||
let name = &name_buf[..len];
|
||||
let handed = answer(UNLOCK, [reply::OK, flags, id, role])
|
||||
&& answer(KEY, words(&key.0))
|
||||
&& answer(SEED, words(&seed))
|
||||
&& answer(NAME, types::log::text_words(name));
|
||||
seed.fill(0);
|
||||
if handed {
|
||||
for gone in removed.iter().flatten() {
|
||||
if !answer(REMOVED, [reply::OK, usize::from_le_bytes(gone.0), 0, 0]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = answer(REMOVED, [reply::DONE, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
Err(why) => {
|
||||
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user