diff --git a/crates/keystore/src/lib.rs b/crates/keystore/src/lib.rs index 12c1047..3912fb9 100644 --- a/crates/keystore/src/lib.rs +++ b/crates/keystore/src/lib.rs @@ -1,23 +1,29 @@ -//! The keystore's contents (see the vision's Gatekeeper note): two sealed -//! key slots, the failed-attempt counter, and the wipe state, in a record -//! kept twice so a torn write never loses both. Pure: the gatekeeper does -//! the I/O; the crypto is RustCrypto's (argon2id, ChaCha20-Poly1305, -//! BLAKE2s). +//! The keystore's contents (see the vision's Gatekeeper and Users notes): +//! eight sealed slots, the user directory, the failed-attempt counter and +//! the wipe state, in a record kept twice so a torn write never loses +//! both. Pure: the gatekeeper does the I/O; the crypto is RustCrypto's +//! (argon2id, ChaCha20-Poly1305, BLAKE2s). //! -//! One slot seals the master key under the real passcode, the other a -//! "wipe" marker under the duress passcode (or is plain random bytes when -//! there is none). Which is which is random, and both look like noise: -//! nothing on the device tells them apart, and every unlock tries both. +//! A passcode is stretched once (argon2id, with the device's salt) and the +//! result tried on every slot. A user's slot seals their master key, role, +//! name and id (and, for an owner, the admin key that opens the +//! directory); the duress slot seals a wipe marker; the rest are noise. +//! Nothing on the device tells the slots apart, or how many users there +//! are: the directory listing them opens only to an owner. #![no_std] #![forbid(unsafe_code)] mod record; +mod user; #[cfg(test)] mod tests; -pub use record::{Copies, KdfParams, RECORD_SIZE, Record, State}; +pub use record::{ + Copies, Entry, KdfParams, Outcome, RECORD_SIZE, Record, Refused, SLOTS, Session, State, +}; +pub use user::{Name, Role, User, UserId}; use argon2::{Algorithm, Argon2, Params, Version}; use chacha20poly1305::aead::{AeadInPlace, KeyInit}; @@ -27,17 +33,12 @@ use zeroize::Zeroize; pub use argon2::Block; pub const KEY_SIZE: usize = 32; -const SALT_SIZE: usize = 16; -const NONCE_SIZE: usize = 12; -const TAG_SIZE: usize = 16; -/// What a slot seals: its kind, then a key. -const SEALED_SIZE: usize = 1 + KEY_SIZE; -const SLOT_SIZE: usize = SALT_SIZE + NONCE_SIZE + SEALED_SIZE + TAG_SIZE; -const REAL: u8 = 1; -const DURESS: u8 = 2; +pub(crate) const SALT_SIZE: usize = 16; +pub(crate) const NONCE_SIZE: usize = 12; +pub(crate) const TAG_SIZE: usize = 16; -/// The master key, wiped from memory when dropped. Compared only in tests: -/// a plain `==` on secrets would not be constant-time. +/// A key, wiped from memory when dropped. Compared only in tests: a plain +/// `==` on secrets would not be constant-time. #[derive(Clone)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct Key(pub [u8; KEY_SIZE]); @@ -54,22 +55,18 @@ impl core::fmt::Debug for Key { } } -/// What a passcode turned out to be. -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub enum Outcome { - Real(Key), - Duress, - Wrong, -} - /// Fills a buffer with randomness; false if it could not. pub trait Random { fn fill(&mut self, buf: &mut [u8]) -> bool; } /// argon2id over the caller's memory (`params.memory_kib` blocks): no heap. -fn derive(passcode: &[u8], salt: &[u8], params: KdfParams, memory: &mut [Block]) -> Option { +pub(crate) fn stretch( + passcode: &[u8], + salt: &[u8], + params: KdfParams, + memory: &mut [Block], +) -> Option { let p = Params::new(params.memory_kib, params.passes, 1, Some(KEY_SIZE)).ok()?; let memory = memory.get_mut(..params.memory_kib as usize)?; let mut out = Key([0; KEY_SIZE]); @@ -79,146 +76,48 @@ fn derive(passcode: &[u8], salt: &[u8], params: KdfParams, memory: &mut [Block]) Some(out) } -/// A slot: salt, nonce, the sealed kind and key, the tag. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct Slot(pub [u8; SLOT_SIZE]); - -impl Slot { - fn random(rng: &mut impl Random) -> Option { - let mut s = [0u8; SLOT_SIZE]; - rng.fill(&mut s).then_some(Self(s)) +/// Seals `data` (in place) under `key` into `nonce | data | tag`. +pub(crate) fn seal(key: &Key, out: &mut [u8], rng: &mut impl Random) -> bool { + let (nonce, rest) = out.split_at_mut(NONCE_SIZE); + let Some(split) = rest.len().checked_sub(TAG_SIZE) else { + return false; + }; + let (data, tag) = rest.split_at_mut(split); + if !rng.fill(nonce) { + return false; } - - /// `kind` and `key`, sealed under a key derived from `passcode`. - fn seal( - kind: u8, - key: &Key, - passcode: &[u8], - params: KdfParams, - memory: &mut [Block], - rng: &mut impl Random, - ) -> Option { - let mut s = [0u8; SLOT_SIZE]; - let (salt, rest) = s.split_at_mut(SALT_SIZE); - let (nonce, rest) = rest.split_at_mut(NONCE_SIZE); - let (sealed, tag) = rest.split_at_mut(SEALED_SIZE); - if !rng.fill(salt) || !rng.fill(nonce) { - return None; + match ChaCha20Poly1305::new((&key.0).into()).encrypt_in_place_detached( + Nonce::from_slice(nonce), + b"", + data, + ) { + Ok(t) => { + tag.copy_from_slice(&t); + true } - let kek = derive(passcode, salt, params, memory)?; - sealed[0] = kind; - sealed[1..].copy_from_slice(&key.0); - let t = ChaCha20Poly1305::new((&kek.0).into()) - .encrypt_in_place_detached(Nonce::from_slice(nonce), b"", sealed) - .ok()?; - tag.copy_from_slice(&t); - Some(Self(s)) + Err(_) => false, } +} - /// What the slot seals, if `passcode` opens it. - fn open(&self, passcode: &[u8], params: KdfParams, memory: &mut [Block]) -> Option<(u8, Key)> { - let (salt, rest) = self.0.split_at(SALT_SIZE); - let (nonce, rest) = rest.split_at(NONCE_SIZE); - let (sealed, tag) = rest.split_at(SEALED_SIZE); - let kek = derive(passcode, salt, params, memory)?; - let mut plain = [0u8; SEALED_SIZE]; - plain.copy_from_slice(sealed); - let opened = ChaCha20Poly1305::new((&kek.0).into()) - .decrypt_in_place_detached( - Nonce::from_slice(nonce), - b"", - &mut plain, - Tag::from_slice(tag), - ) - .is_ok(); - let mut key = Key([0; KEY_SIZE]); - key.0.copy_from_slice(&plain[1..]); - let kind = plain[0]; +/// Opens what `seal` made into `plain` (its data's length); false (and +/// `plain` wiped) unless `key` sealed exactly this. +pub(crate) fn open(key: &Key, sealed: &[u8], plain: &mut [u8]) -> bool { + let Some((nonce, rest)) = sealed.split_at_checked(NONCE_SIZE) else { + return false; + }; + let Some(split) = rest.len().checked_sub(TAG_SIZE) else { + return false; + }; + let (data, tag) = rest.split_at(split); + let Some(plain) = plain.get_mut(..data.len()) else { + return false; + }; + plain.copy_from_slice(data); + let ok = ChaCha20Poly1305::new((&key.0).into()) + .decrypt_in_place_detached(Nonce::from_slice(nonce), b"", plain, Tag::from_slice(tag)) + .is_ok(); + if !ok { plain.zeroize(); - opened.then_some((kind, key)) - } -} - -impl Record { - /// A fresh keystore: a new random master key, sealed under `real`; a - /// duress slot under `duress`, or noise. Returns it and the master key. - pub fn setup( - real: &[u8], - duress: Option<&[u8]>, - threshold: u8, - params: KdfParams, - memory: &mut [Block], - rng: &mut impl Random, - ) -> Option<(Self, Key)> { - let mut master = Key([0; KEY_SIZE]); - if !rng.fill(&mut master.0) { - return None; - } - let real_slot = Slot::seal(REAL, &master, real, params, memory, rng)?; - let other = match duress { - Some(d) => { - // Its key is never used: random, so the slot looks like the other. - let mut filler = Key([0; KEY_SIZE]); - if !rng.fill(&mut filler.0) { - return None; - } - Slot::seal(DURESS, &filler, d, params, memory, rng)? - } - None => Slot::random(rng)?, - }; - let mut coin = [0u8; 1]; - if !rng.fill(&mut coin) { - return None; - } - let slots = if coin[0] & 1 == 0 { - [real_slot, other] - } else { - [other, real_slot] - }; - let record = Self { - generation: 1, - state: State::Active, - threshold: threshold.max(1), - failures: 0, - params, - slots, - }; - Some((record, master)) - } - - /// Tries `passcode` on both slots, always both, so the time taken does - /// not tell which (if any) it opened. - pub fn unlock(&self, passcode: &[u8], memory: &mut [Block]) -> Outcome { - let a = self.slots[0].open(passcode, self.params, memory); - let b = self.slots[1].open(passcode, self.params, memory); - match a.or(b) { - Some((REAL, key)) => Outcome::Real(key), - Some((DURESS, _)) => Outcome::Duress, - _ => Outcome::Wrong, - } - } - - /// The same record, one later, with `changes` made. - pub fn next(&self, changes: impl FnOnce(&mut Self)) -> Self { - let mut r = *self; - r.generation = self.generation.wrapping_add(1); - changes(&mut r); - r - } - - /// This record with both slots overwritten by noise and marked as - /// being wiped, so an interrupted wipe is finished on the next boot. - pub fn wiping(&self, rng: &mut impl Random) -> Option { - let slots = [Slot::random(rng)?, Slot::random(rng)?]; - Some(self.next(|r| { - r.state = State::Wiping; - r.slots = slots; - })) - } - - /// Whether this record must be wiped before anything else: a wipe was - /// interrupted, or too many wrong passcodes. - pub fn must_wipe(&self) -> bool { - self.state == State::Wiping || self.failures >= self.threshold } + ok } diff --git a/crates/keystore/src/record.rs b/crates/keystore/src/record.rs index d51533c..c03f93e 100644 --- a/crates/keystore/src/record.rs +++ b/crates/keystore/src/record.rs @@ -1,14 +1,31 @@ -//! The record's bytes, and the two copies it is kept in. +//! The record: header, the sealed user directory, the slots, a checksum; +//! its bytes, and the two copies it is kept in. -use blake2::{Blake2s256, Digest}; +use blake2::{Blake2s256, Blake2sMac256, Digest, digest::Mac}; +use chacha20poly1305::aead::KeyInit; +use zeroize::Zeroize; -use crate::{SLOT_SIZE, Slot}; +use crate::user::{Name, Role, User, UserId}; +use crate::{Block, KEY_SIZE, Key, NONCE_SIZE, Random, SALT_SIZE, TAG_SIZE, open, seal, stretch}; -/// One record fills one sector. -pub const RECORD_SIZE: usize = 512; -const MAGIC: &[u8; 8] = b"KOSKEYS1"; +/// One record fills four sectors. +pub const RECORD_SIZE: usize = 2048; +pub const SLOTS: usize = 8; +const MAGIC: &[u8; 8] = b"KOSKEYS2"; +const NAME_TAG: usize = 16; +/// What a slot seals: kind, role, user id, name, master key, admin key +/// (an owner's; noise for anyone else). +const PLAIN: usize = 1 + 1 + 8 + Name::MAX + KEY_SIZE + KEY_SIZE; +const SLOT: usize = NAME_TAG + SALT_SIZE + NONCE_SIZE + PLAIN + TAG_SIZE; +/// One directory entry: kind (0 free), role, user id, name. +const ENTRY: usize = 1 + 1 + 8 + Name::MAX; +const DIRECTORY: usize = NONCE_SIZE + SLOTS * ENTRY + TAG_SIZE; +const HEADER: usize = 8 + 8 + 4 + 8 + SALT_SIZE; +const BODY: usize = HEADER + DIRECTORY + SLOTS * SLOT; const CHECKSUM: usize = 32; -const BODY: usize = 8 + 8 + 4 + 8 + 2 * SLOT_SIZE; +const FREE: u8 = 0; +const USER: u8 = 1; +const DURESS: u8 = 2; /// How slow a passcode guess is: argon2id's memory (KiB) and passes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -32,18 +49,69 @@ pub enum State { Wiping, } +/// A signed-in user: who, their master key, which slot holds them, and, +/// for an owner, the admin key that opens the directory. +pub struct Session { + pub user: User, + pub key: Key, + pub slot: usize, + admin: Option, +} + +impl Session { + pub fn is_owner(&self) -> bool { + self.admin.is_some() + } +} + +/// What a name and passcode turned out to be. +pub enum Outcome { + User(Session), + Duress, + Wrong, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Refused { + NameTaken, + /// No free slots (a user takes one, two with a duress passcode). + Full, + NoSuchUser, + /// Only an owner manages users. + NotOwner, + /// It would leave the device without an owner. + LastOwner, + /// No randomness, the KDF failed, or the record does not open. + Failed, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct Slot([u8; SLOT]); + +/// A directory entry, as an owner reads it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Entry { + duress: bool, + pub user: User, +} + #[derive(Clone, Copy, PartialEq, Eq)] pub struct Record { /// Which copy is newer: the higher one. pub generation: u64, pub state: State, - /// Wrong passcodes in a row that wipe the keys. + /// Wrong passcodes in a row (for any name) that wipe every user. pub threshold: u8, /// Wrong passcodes in a row so far; counted before a passcode is /// checked, so pulling the power cannot save a guess. pub failures: u8, + /// A guest may sign in (with an empty name). + pub guests: bool, pub params: KdfParams, - pub slots: [Slot; 2], + /// Keys the name tags, so names are not written out. + pepper: [u8; SALT_SIZE], + directory: [u8; DIRECTORY], + slots: [Slot; SLOTS], } impl core::fmt::Debug for Record { @@ -52,16 +120,412 @@ impl core::fmt::Debug for Record { .field("generation", &self.generation) .field("state", &self.state) .field("failures", &self.failures) - .field("threshold", &self.threshold) + .field("guests", &self.guests) .finish_non_exhaustive() } } -fn checksum(body: &[u8]) -> [u8; CHECKSUM] { - Blake2s256::digest(body).into() +fn noise(rng: &mut impl Random) -> Option<[u8; N]> { + let mut b = [0u8; N]; + rng.fill(&mut b).then_some(b) +} + +/// The plaintext a slot seals. +fn slot_plain(kind: u8, user: &User, key: &Key, admin: &Key) -> [u8; PLAIN] { + let mut p = [0u8; PLAIN]; + p[0] = kind; + p[1] = user.role.byte(); + p[2..10].copy_from_slice(&user.id.0); + p[10..10 + Name::MAX].copy_from_slice(&user.name.raw()); + p[10 + Name::MAX..10 + Name::MAX + KEY_SIZE].copy_from_slice(&key.0); + p[10 + Name::MAX + KEY_SIZE..].copy_from_slice(&admin.0); + p +} + +fn parse_user(role: u8, id: &[u8], name: &[u8]) -> Option { + let mut i = [0u8; 8]; + i.copy_from_slice(id); + let mut n = [0u8; Name::MAX]; + n.copy_from_slice(name); + Some(User { + id: UserId(i), + role: Role::from_byte(role)?, + name: Name::from_raw(n)?, + }) } impl Record { + fn name_tag(&self, name: &Name) -> [u8; NAME_TAG] { + let mut m = ::new_from_slice(&self.pepper) + .expect("BLAKE2s takes a 16-byte key"); + m.update(b"kos/keystore/v2/name/"); + m.update(name.as_bytes()); + let mut tag = [0u8; NAME_TAG]; + tag.copy_from_slice(&m.finalize().into_bytes()[..NAME_TAG]); + tag + } + + /// `plain` sealed into slot `i` for the name `tag`, under `passcode`. + fn seal_slot( + &mut self, + i: usize, + tag: &[u8; NAME_TAG], + plain: &[u8; PLAIN], + passcode: &[u8], + memory: &mut [Block], + rng: &mut impl Random, + ) -> bool { + let mut s = [0u8; SLOT]; + s[..NAME_TAG].copy_from_slice(tag); + let (salt, body) = s[NAME_TAG..].split_at_mut(SALT_SIZE); + if !rng.fill(salt) { + return false; + } + let Some(kek) = stretch(passcode, salt, self.params, memory) else { + return false; + }; + body[NONCE_SIZE..NONCE_SIZE + PLAIN].copy_from_slice(plain); + let ok = seal(&kek, body, rng); + self.slots[i] = Slot(s); + ok + } + + /// The directory, opened with an owner's admin key. + fn entries(&self, admin: &Key) -> Option<[Option; SLOTS]> { + let mut plain = [0u8; SLOTS * ENTRY]; + if !open(admin, &self.directory, &mut plain) { + return None; + } + let out = core::array::from_fn(|i| { + let e = &plain[i * ENTRY..(i + 1) * ENTRY]; + match e[0] { + FREE => None, + kind => parse_user(e[1], &e[2..10], &e[10..]).map(|user| Entry { + duress: kind == DURESS, + user, + }), + } + }); + plain.zeroize(); + Some(out) + } + + fn seal_directory( + &mut self, + admin: &Key, + entries: &[Option; SLOTS], + rng: &mut impl Random, + ) -> bool { + let mut d = [0u8; DIRECTORY]; + for (i, e) in entries.iter().enumerate() { + let at = NONCE_SIZE + i * ENTRY; + if let Some(e) = e { + d[at] = if e.duress { DURESS } else { USER }; + d[at + 1] = e.user.role.byte(); + d[at + 2..at + 10].copy_from_slice(&e.user.id.0); + d[at + 10..at + ENTRY].copy_from_slice(&e.user.name.raw()); + } + } + let ok = seal(admin, &mut d, rng); + self.directory = d; + ok + } + + /// A device with one owner, `name`, and (if given) their duress + /// passcode. Returns the record and the owner's session. + pub fn setup( + name: Name, + passcode: &[u8], + duress: Option<&[u8]>, + threshold: u8, + params: KdfParams, + memory: &mut [Block], + rng: &mut impl Random, + ) -> Option<(Self, Session)> { + let mut slots = [Slot([0; SLOT]); SLOTS]; + for s in slots.iter_mut() { + *s = Slot(noise(rng)?); + } + let empty = Self { + generation: 1, + state: State::Active, + threshold: threshold.max(1), + failures: 0, + guests: false, + params, + pepper: noise(rng)?, + directory: [0; DIRECTORY], + slots, + }; + let admin = Key(noise(rng)?); + let mut first = empty; + if !first.seal_directory(&admin, &[None; SLOTS], rng) { + return None; + } + let owner = Session { + user: User { + id: UserId([0; 8]), + role: Role::Owner, + name, + }, + key: Key([0; KEY_SIZE]), + slot: 0, + admin: Some(admin), + }; + let (record, user) = first + .add_user(&owner, name, Role::Owner, passcode, duress, memory, rng) + .ok()?; + let session = record.unlock(name.as_bytes(), passcode, memory); + let _ = user; + match session { + Outcome::User(s) => Some((record, s)), + _ => None, + } + } + + /// Tries `name` and `passcode`. Always does the same work, two KDF + /// runs, whether the name exists or not, so the time taken tells + /// nothing. + pub fn unlock(&self, name: &[u8], passcode: &[u8], memory: &mut [Block]) -> Outcome { + let tag = Name::new(name).map(|n| self.name_tag(&n)); + let mut candidates = [None; 2]; + let mut n = 0; + for i in 0..SLOTS { + if tag.is_some_and(|t| self.slots[i].0[..NAME_TAG] == t) && n < 2 { + candidates[n] = Some(i); + n += 1; + } + } + let mut found = None; + for c in candidates { + // A missing candidate still costs a KDF run, over a fake salt. + let slot = c.map_or([0u8; SLOT], |i| self.slots[i].0); + let (salt, body) = slot[NAME_TAG..].split_at(SALT_SIZE); + let Some(kek) = stretch(passcode, salt, self.params, memory) else { + continue; + }; + let mut plain = [0u8; PLAIN]; + if c.is_some() && open(&kek, body, &mut plain) && found.is_none() { + found = c.map(|i| (i, plain)); + } + plain.zeroize(); + } + let Some((slot, mut plain)) = found else { + return Outcome::Wrong; + }; + let outcome = match ( + plain[0], + parse_user(plain[1], &plain[2..10], &plain[10..10 + Name::MAX]), + ) { + (DURESS, _) => Outcome::Duress, + (USER, Some(user)) => { + let mut key = Key([0; KEY_SIZE]); + key.0 + .copy_from_slice(&plain[10 + Name::MAX..10 + Name::MAX + KEY_SIZE]); + let mut admin = Key([0; KEY_SIZE]); + admin.0.copy_from_slice(&plain[10 + Name::MAX + KEY_SIZE..]); + let admin = (user.role == Role::Owner).then_some(admin); + Outcome::User(Session { + user, + key, + slot, + admin, + }) + } + _ => Outcome::Wrong, + }; + plain.zeroize(); + outcome + } + + /// What `name`'s slot carries as the admin key, opened with `passcode`. + #[cfg(test)] + pub(crate) fn carried_admin( + &self, + name: &[u8], + passcode: &[u8], + memory: &mut [Block], + ) -> Option { + let tag = self.name_tag(&Name::new(name)?); + let slot = self.slots.iter().find(|s| s.0[..NAME_TAG] == tag)?; + let (salt, body) = slot.0[NAME_TAG..].split_at(SALT_SIZE); + let kek = stretch(passcode, salt, self.params, memory)?; + let mut plain = [0u8; PLAIN]; + open(&kek, body, &mut plain).then(|| { + let mut k = Key([0; KEY_SIZE]); + k.0.copy_from_slice(&plain[10 + Name::MAX + KEY_SIZE..]); + k + }) + } + + #[cfg(test)] + pub(crate) fn admin_of(s: &Session) -> Option { + s.admin.clone() + } + + /// Who has an account (their duress slots left out); only for an owner. + pub fn users(&self, by: &Session) -> Result<[Option; SLOTS], Refused> { + let admin = by.admin.as_ref().ok_or(Refused::NotOwner)?; + let entries = self.entries(admin).ok_or(Refused::Failed)?; + Ok(entries.map(|e| e.filter(|e| !e.duress).map(|e| e.user))) + } + + /// An owner adds a user; the new record and the user. + pub fn add_user( + &self, + by: &Session, + name: Name, + role: Role, + passcode: &[u8], + duress: Option<&[u8]>, + memory: &mut [Block], + rng: &mut impl Random, + ) -> Result<(Self, User), Refused> { + let admin = by.admin.clone().ok_or(Refused::NotOwner)?; + let mut entries = self.entries(&admin).ok_or(Refused::Failed)?; + if entries.iter().flatten().any(|e| e.user.name == name) { + return Err(Refused::NameTaken); + } + let need = 1 + usize::from(duress.is_some()); + let free: [usize; SLOTS] = core::array::from_fn(|i| i); + let mut free = free.into_iter().filter(|&i| entries[i].is_none()); + let (Some(real), spare) = (free.next(), free.next()) else { + return Err(Refused::Full); + }; + if need == 2 && spare.is_none() { + return Err(Refused::Full); + } + let user = User { + id: UserId(noise(rng).ok_or(Refused::Failed)?), + role, + name, + }; + let key = Key(noise(rng).ok_or(Refused::Failed)?); + // Only an owner's slot carries the admin key; anyone else's, noise. + let carried = match role { + Role::Owner => admin.clone(), + Role::Standard => Key(noise(rng).ok_or(Refused::Failed)?), + }; + let mut r = self.next(|_| {}); + let tag = r.name_tag(&name); + if !r.seal_slot( + real, + &tag, + &slot_plain(USER, &user, &key, &carried), + passcode, + memory, + rng, + ) { + return Err(Refused::Failed); + } + entries[real] = Some(Entry { + duress: false, + user, + }); + if let (Some(d), Some(i)) = (duress, spare) { + let filler = Key(noise(rng).ok_or(Refused::Failed)?); + if !r.seal_slot( + i, + &tag, + &slot_plain(DURESS, &user, &filler, &filler), + d, + memory, + rng, + ) { + return Err(Refused::Failed); + } + entries[i] = Some(Entry { duress: true, user }); + } + if !r.seal_directory(&admin, &entries, rng) { + return Err(Refused::Failed); + } + Ok((r, user)) + } + + /// An owner removes the user `name` (their slots become noise); the + /// new record and the removed user's id, whose files are to go too. + pub fn remove_user( + &self, + by: &Session, + name: &Name, + rng: &mut impl Random, + ) -> Result<(Self, UserId), Refused> { + let admin = by.admin.clone().ok_or(Refused::NotOwner)?; + let mut entries = self.entries(&admin).ok_or(Refused::Failed)?; + let gone = entries + .iter() + .flatten() + .find(|e| e.user.name == *name) + .map(|e| e.user) + .ok_or(Refused::NoSuchUser)?; + let owners_left = entries + .iter() + .flatten() + .filter(|e| !e.duress && e.user.role == Role::Owner && e.user.id != gone.id) + .count(); + if gone.role == Role::Owner && owners_left == 0 { + return Err(Refused::LastOwner); + } + let mut r = self.next(|_| {}); + for i in 0..SLOTS { + if entries[i].is_some_and(|e| e.user.id == gone.id) { + r.slots[i] = Slot(noise(rng).ok_or(Refused::Failed)?); + entries[i] = None; + } + } + if !r.seal_directory(&admin, &entries, rng) { + return Err(Refused::Failed); + } + Ok((r, gone.id)) + } + + /// The signed-in user picks a new passcode: their slot is resealed. + pub fn change_passcode( + &self, + by: &Session, + passcode: &[u8], + memory: &mut [Block], + rng: &mut impl Random, + ) -> Result { + let carried = match &by.admin { + Some(a) => a.clone(), + None => Key(noise(rng).ok_or(Refused::Failed)?), + }; + let mut r = self.next(|_| {}); + let tag = r.name_tag(&by.user.name); + let plain = slot_plain(USER, &by.user, &by.key, &carried); + if !r.seal_slot(by.slot, &tag, &plain, passcode, memory, rng) { + return Err(Refused::Failed); + } + Ok(r) + } + + /// The same record, one later, with `changes` made. + pub fn next(&self, changes: impl FnOnce(&mut Self)) -> Self { + let mut r = *self; + r.generation = self.generation.wrapping_add(1); + changes(&mut r); + r + } + + /// Everything sealed overwritten by noise, marked as being wiped, so an + /// interrupted wipe is finished on the next boot. + pub fn wiping(&self, rng: &mut impl Random) -> Option { + let mut r = self.next(|r| r.state = State::Wiping); + r.directory = noise(rng)?; + r.pepper = noise(rng)?; + for s in r.slots.iter_mut() { + *s = Slot(noise(rng)?); + } + Some(r) + } + + /// Whether this record must be wiped before anything else: a wipe was + /// interrupted, or too many wrong passcodes. + pub fn must_wipe(&self) -> bool { + self.state == State::Wiping || self.failures >= self.threshold + } + pub fn encode(&self) -> [u8; RECORD_SIZE] { let mut out = [0u8; RECORD_SIZE]; out[0..8].copy_from_slice(MAGIC); @@ -72,11 +536,16 @@ impl Record { }; out[17] = self.threshold; out[18] = self.failures; + out[19] = u8::from(self.guests); out[20..24].copy_from_slice(&self.params.memory_kib.to_le_bytes()); out[24..28].copy_from_slice(&self.params.passes.to_le_bytes()); - out[28..28 + SLOT_SIZE].copy_from_slice(&self.slots[0].0); - out[28 + SLOT_SIZE..BODY].copy_from_slice(&self.slots[1].0); - let sum = checksum(&out[..BODY]); + out[28..HEADER].copy_from_slice(&self.pepper); + out[HEADER..HEADER + DIRECTORY].copy_from_slice(&self.directory); + for (i, s) in self.slots.iter().enumerate() { + let at = HEADER + DIRECTORY + i * SLOT; + out[at..at + SLOT].copy_from_slice(&s.0); + } + let sum: [u8; CHECKSUM] = Blake2s256::digest(&out[..BODY]).into(); out[BODY..BODY + CHECKSUM].copy_from_slice(&sum); out } @@ -84,7 +553,8 @@ impl Record { /// `None` unless it is a whole, unaltered record (magic and checksum). pub fn decode(bytes: &[u8]) -> Option { let bytes = bytes.get(..RECORD_SIZE)?; - if &bytes[0..8] != MAGIC || bytes[BODY..BODY + CHECKSUM] != checksum(&bytes[..BODY]) { + let sum: [u8; CHECKSUM] = Blake2s256::digest(&bytes[..BODY]).into(); + if &bytes[0..8] != MAGIC || bytes[BODY..BODY + CHECKSUM] != sum { return None; } let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap_or([0; 4])); @@ -93,20 +563,24 @@ impl Record { 2 => State::Wiping, _ => return None, }; - let mut a = [0u8; SLOT_SIZE]; - let mut b = [0u8; SLOT_SIZE]; - a.copy_from_slice(&bytes[28..28 + SLOT_SIZE]); - b.copy_from_slice(&bytes[28 + SLOT_SIZE..BODY]); + let mut slots = [Slot([0; SLOT]); SLOTS]; + for (i, s) in slots.iter_mut().enumerate() { + let at = HEADER + DIRECTORY + i * SLOT; + s.0.copy_from_slice(&bytes[at..at + SLOT]); + } Some(Self { generation: u64::from_le_bytes(bytes[8..16].try_into().ok()?), state, threshold: bytes[17], failures: bytes[18], - params: crate::KdfParams { + guests: bytes[19] != 0, + params: KdfParams { memory_kib: word(20), passes: word(24), }, - slots: [Slot(a), Slot(b)], + pepper: bytes[28..HEADER].try_into().ok()?, + directory: bytes[HEADER..HEADER + DIRECTORY].try_into().ok()?, + slots, }) } } @@ -135,3 +609,5 @@ impl Copies<'_> { self.newest().map_or(0, |(_, copy)| 1 - copy) } } + +const _: () = assert!(BODY + CHECKSUM <= RECORD_SIZE); diff --git a/crates/keystore/src/tests.rs b/crates/keystore/src/tests.rs index 00d7480..dd2f62b 100644 --- a/crates/keystore/src/tests.rs +++ b/crates/keystore/src/tests.rs @@ -23,150 +23,294 @@ impl Random for Counter { } } -struct Broken; - -impl Random for Broken { - fn fill(&mut self, _buf: &mut [u8]) -> bool { - false - } -} - fn memory() -> Vec { vec![Block::default(); FAST.memory_kib as usize] } -fn setup(duress: Option<&[u8]>, seed: u8) -> (Record, Key) { - Record::setup(b"1234", duress, 5, FAST, &mut memory(), &mut Counter(seed)).unwrap() +fn name(s: &str) -> Name { + Name::new(s.as_bytes()).unwrap() } -#[test] -fn the_real_passcode_opens_the_master_key_and_nothing_else_does() { - let (record, master) = setup(Some(b"0000"), 1); - let mem = &mut memory(); - assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master)); - assert_eq!(record.unlock(b"0000", mem), Outcome::Duress); - for wrong in [&b"1235"[..], b"", b"12345", b"123"] { - assert_eq!(record.unlock(wrong, mem), Outcome::Wrong, "{wrong:?}"); +fn setup(duress: Option<&[u8]>, seed: u8) -> (Record, Session) { + Record::setup( + name("gabriel"), + b"1234", + duress, + 5, + FAST, + &mut memory(), + &mut Counter(seed), + ) + .unwrap() +} + +fn signed_in(r: &Record, who: &str, passcode: &[u8]) -> Option { + match r.unlock(who.as_bytes(), passcode, &mut memory()) { + Outcome::User(s) => Some(s), + _ => None, } } +fn is_wrong(o: Outcome) -> bool { + matches!(o, Outcome::Wrong) +} + #[test] -fn without_a_duress_passcode_its_slot_opens_for_nothing() { - let (record, master) = setup(None, 2); - let mem = &mut memory(); - assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master)); - assert_eq!(record.unlock(b"0000", mem), Outcome::Wrong); +fn the_owner_signs_in_with_name_and_passcode() { + let (r, owner) = setup(Some(b"0000"), 1); + assert!(owner.is_owner()); + let again = signed_in(&r, "gabriel", b"1234").unwrap(); + assert_eq!(again.key, owner.key, "the same master key every time"); + assert_eq!(again.user, owner.user); + assert!(matches!( + r.unlock(b"gabriel", b"0000", &mut memory()), + Outcome::Duress + )); + assert!(is_wrong(r.unlock(b"gabriel", b"1235", &mut memory()))); + assert!( + is_wrong(r.unlock(b"nobody", b"1234", &mut memory())), + "an unknown name is just wrong" + ); + assert!(is_wrong(r.unlock(b"", b"1234", &mut memory()))); +} + +#[test] +fn users_have_their_own_keys_and_may_even_share_a_passcode() { + let (r, owner) = setup(None, 2); + let mut rng = Counter(40); + let (r, work) = r + .add_user( + &owner, + name("work"), + Role::Standard, + b"1234", + None, + &mut memory(), + &mut rng, + ) + .unwrap(); + let w = signed_in(&r, "work", b"1234").unwrap(); + let g = signed_in(&r, "gabriel", b"1234").unwrap(); + assert_eq!(w.user, work); + assert_ne!( + w.key, g.key, + "same passcode, different users, different keys" + ); + assert_ne!(w.user.id, g.user.id); + assert!(!w.is_owner(), "a standard user holds no admin key"); + let admin = Record::admin_of(&owner).unwrap(); + assert_ne!( + r.carried_admin(b"work", b"1234", &mut memory()), + Some(admin), + "not even hidden in their slot" + ); assert_eq!( - record.unlock(b"", mem), - Outcome::Wrong, - "not even an empty one" + r.add_user( + &w, + name("x"), + Role::Standard, + b"9", + None, + &mut memory(), + &mut rng + ) + .err(), + Some(Refused::NotOwner) + ); + assert_eq!(r.users(&w).err(), Some(Refused::NotOwner)); +} + +#[test] +fn owners_see_and_manage_the_users() { + let (r, owner) = setup(Some(b"0000"), 3); + let mut rng = Counter(50); + let (r, _) = r + .add_user( + &owner, + name("work"), + Role::Standard, + b"5678", + Some(b"8765"), + &mut memory(), + &mut rng, + ) + .unwrap(); + let (r, second) = r + .add_user( + &owner, + name("anna"), + Role::Owner, + b"4321", + None, + &mut memory(), + &mut rng, + ) + .unwrap(); + let names: Vec<_> = r + .users(&owner) + .unwrap() + .iter() + .flatten() + .map(|u| std::string::String::from_utf8_lossy(u.name.as_bytes()).into_owned()) + .collect(); + assert_eq!(names.len(), 3, "duress slots are not listed: {names:?}"); + assert_eq!( + r.add_user( + &owner, + name("work"), + Role::Standard, + b"x", + None, + &mut memory(), + &mut rng + ) + .err(), + Some(Refused::NameTaken) + ); + // A second owner manages users too: the admin key reached them. + let anna = signed_in(&r, "anna", b"4321").unwrap(); + assert_eq!(anna.user, second); + assert!(r.users(&anna).is_ok()); + // Work's duress passcode wipes; it is work's, nobody else's. + assert!(matches!( + r.unlock(b"work", b"8765", &mut memory()), + Outcome::Duress + )); + assert!(is_wrong(r.unlock(b"gabriel", b"8765", &mut memory()))); + // Removing work removes both of work's slots. + let (r, gone) = r.remove_user(&owner, &name("work"), &mut rng).unwrap(); + assert!(signed_in(&r, "work", b"5678").is_none()); + assert!(is_wrong(r.unlock(b"work", b"8765", &mut memory()))); + assert_ne!(gone, owner.user.id); + assert_eq!( + r.remove_user(&owner, &name("work"), &mut rng).err(), + Some(Refused::NoSuchUser) ); } #[test] -fn which_slot_is_real_is_not_fixed() { - // Different randomness puts the real slot first in some setups, second - // in others: its position gives nothing away. - let mem = &mut memory(); - let mut firsts = [false; 2]; - for seed in 0..16 { - let (record, _) = setup(Some(b"0000"), seed); - let only_first = Record { - slots: [record.slots[0], Slot(record.slots[1].0.map(|b| !b))], - ..record - }; - firsts[matches!(only_first.unlock(b"1234", mem), Outcome::Real(_)) as usize] = true; +fn the_last_owner_stays() { + let (r, owner) = setup(None, 4); + assert_eq!( + r.remove_user(&owner, &name("gabriel"), &mut Counter(1)) + .err(), + Some(Refused::LastOwner) + ); +} + +#[test] +fn slots_run_out() { + let (mut r, owner) = setup(Some(b"0000"), 5); + let mut rng = Counter(60); + let mut added = 0; + for i in 0..SLOTS { + let n = std::format!("u{i}"); + match r.add_user( + &owner, + name(&n), + Role::Standard, + b"pass", + None, + &mut memory(), + &mut rng, + ) { + Ok((next, _)) => { + r = next; + added += 1; + } + Err(e) => { + assert_eq!(e, Refused::Full); + break; + } + } } - assert_eq!(firsts, [true, true]); + assert_eq!( + added, + SLOTS - 2, + "the owner and the owner's duress slot take two" + ); +} + +#[test] +fn a_new_passcode_shuts_out_the_old_one_and_keeps_the_key() { + let (r, owner) = setup(None, 6); + let r = r + .change_passcode(&owner, b"9999", &mut memory(), &mut Counter(70)) + .unwrap(); + assert!(signed_in(&r, "gabriel", b"1234").is_none()); + let s = signed_in(&r, "gabriel", b"9999").unwrap(); + assert_eq!(s.key, owner.key, "the files stay readable"); + assert!(s.is_owner(), "and an owner stays an owner"); + assert!(r.users(&s).is_ok(), "who can still manage users"); } #[test] fn records_round_trip_and_damage_is_detected() { - let (record, _) = setup(Some(b"0000"), 3); + let (record, _) = setup(Some(b"0000"), 7); let bytes = record.encode(); assert_eq!(Record::decode(&bytes), Some(record)); - for at in [0, 9, 18, 40, 200] { + for at in [0, 9, 18, 40, 300, 1200] { let mut torn = bytes; torn[at] ^= 1; assert_eq!(Record::decode(&torn), None, "byte {at}"); } - assert_eq!( - Record::decode(&[0; RECORD_SIZE]), - None, - "a blank device holds no record" - ); + assert_eq!(Record::decode(&[0; RECORD_SIZE]), None); } #[test] fn the_newest_whole_copy_wins_and_writes_go_to_the_other() { - let (old, _) = setup(None, 4); + let (old, _) = setup(None, 8); let new = old.next(|r| r.failures = 1); let (a, b) = (old.encode(), new.encode()); - let copies = Copies { copies: [&a, &b] }; - assert_eq!(copies.newest(), Some((new, 1))); - assert_eq!(copies.next_copy(), 0, "the older copy is overwritten"); - // A torn write of the newer copy leaves the older one. + assert_eq!(Copies { copies: [&a, &b] }.newest(), Some((new, 1))); + assert_eq!(Copies { copies: [&b, &a] }.newest(), Some((new, 0))); + assert_eq!(Copies { copies: [&a, &b] }.next_copy(), 0); let mut torn = b; torn[100] ^= 0xFF; - let copies = Copies { - copies: [&a, &torn], - }; - assert_eq!(copies.newest(), Some((old, 0))); - assert_eq!(copies.next_copy(), 1); - // Newest wins wherever it is. - let copies = Copies { copies: [&b, &a] }; - assert_eq!(copies.newest(), Some((new, 0))); - let blank = [0u8; RECORD_SIZE]; assert_eq!( Copies { - copies: [&blank, &blank] + copies: [&a, &torn] } .newest(), - None + Some((old, 0)) ); } #[test] -fn wiping_leaves_nothing_any_passcode_opens() { - let (record, _) = setup(Some(b"0000"), 5); +fn wiping_leaves_nothing_any_name_opens() { + let (record, owner) = setup(Some(b"0000"), 9); let wiped = record.wiping(&mut Counter(99)).unwrap(); - assert_eq!(wiped.state, State::Wiping); - assert!( - wiped.must_wipe(), - "an interrupted wipe is finished next boot" - ); - assert!( - wiped.generation > record.generation, - "it is the newest copy" - ); - let mem = &mut memory(); + assert!(wiped.must_wipe()); + assert!(wiped.generation > record.generation); for p in [&b"1234"[..], b"0000"] { - assert_eq!(wiped.unlock(p, mem), Outcome::Wrong); + assert!(is_wrong(wiped.unlock(b"gabriel", p, &mut memory()))); } + assert_eq!( + wiped.users(&owner).err(), + Some(Refused::Failed), + "nor does the directory open" + ); } #[test] fn too_many_failures_mean_a_wipe() { - let (record, _) = setup(None, 6); + let (record, _) = setup(None, 10); assert!(!record.must_wipe()); - let worn = record.next(|r| r.failures = r.threshold); - assert!(worn.must_wipe()); + assert!(record.next(|r| r.failures = r.threshold).must_wipe()); } #[test] -fn no_randomness_means_no_keystore() { - assert!(Record::setup(b"1234", None, 5, FAST, &mut memory(), &mut Broken).is_none()); +fn names_are_printable_and_short() { + assert!(Name::new(b"").is_none()); + assert!(Name::new(b"has space").is_none()); + assert!(Name::new(&[b'a'; 17]).is_none()); + assert!(Name::new(b"Gabriel_K-1").is_some()); } #[test] -fn the_default_parameters_are_accepted_by_argon2() { - assert!( - argon2::Params::new( - KdfParams::DEFAULT.memory_kib, - KdfParams::DEFAULT.passes, - 1, - Some(KEY_SIZE) - ) - .is_ok() +fn user_ids_name_folders_in_hex() { + assert_eq!( + &UserId([0xde, 0xad, 0xbe, 0xef, 0, 1, 2, 0xff]).hex(), + b"deadbeef000102ff" ); } diff --git a/crates/keystore/src/user.rs b/crates/keystore/src/user.rs new file mode 100644 index 0000000..1f18cfe --- /dev/null +++ b/crates/keystore/src/user.rs @@ -0,0 +1,91 @@ +//! Who a slot belongs to. + +/// Up to 16 bytes of printable ASCII. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Name { + bytes: [u8; Name::MAX], + len: u8, +} + +impl Name { + pub const MAX: usize = 16; + + pub fn new(s: &[u8]) -> Option { + let ok = + !s.is_empty() && s.len() <= Self::MAX && s.iter().all(|b| (b'!'..=b'~').contains(b)); + ok.then(|| { + let mut bytes = [0; Self::MAX]; + bytes[..s.len()].copy_from_slice(s); + Self { + bytes, + len: s.len() as u8, + } + }) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } + + pub(crate) fn raw(&self) -> [u8; Self::MAX] { + self.bytes + } + + pub(crate) fn from_raw(raw: [u8; Self::MAX]) -> Option { + let len = raw.iter().position(|b| *b == 0).unwrap_or(Self::MAX); + Self::new(&raw[..len]) + } +} + +impl core::fmt::Debug for Name { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{:?}", + core::str::from_utf8(self.as_bytes()).unwrap_or("?") + ) + } +} + +/// A user's stable id: random, so their folders give nothing away. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct UserId(pub [u8; 8]); + +impl UserId { + /// As a folder name: 16 lowercase hex digits. + pub fn hex(&self) -> [u8; 16] { + let digits = b"0123456789abcdef"; + core::array::from_fn(|i| digits[(self.0[i / 2] >> (4 * (1 - i % 2)) & 0xF) as usize]) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Role { + /// Manages users at the lock screen. + Owner, + Standard, +} + +impl Role { + pub(crate) fn byte(self) -> u8 { + match self { + Role::Owner => 1, + Role::Standard => 2, + } + } + + pub(crate) fn from_byte(b: u8) -> Option { + match b { + 1 => Some(Role::Owner), + 2 => Some(Role::Standard), + _ => None, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct User { + pub id: UserId, + pub role: Role, + pub name: Name, +} diff --git a/crates/servers/gatekeeper/src/devices.rs b/crates/servers/gatekeeper/src/devices.rs index 89aeb1a..b90b84c 100644 --- a/crates/servers/gatekeeper/src/devices.rs +++ b/crates/servers/gatekeeper/src/devices.rs @@ -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(pub D); +impl Disk { + /// Sector `i` of copy `copy`, for each of the sectors a record takes. + fn sectors(&self, copy: usize) -> impl Iterator { + 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 Keystore for Disk { 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::>().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::>().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(); diff --git a/crates/servers/gatekeeper/src/flow.rs b/crates/servers/gatekeeper/src/flow.rs index b61a1ec..3b61844 100644 --- a/crates/servers/gatekeeper/src/flow.rs +++ b/crates/servers/gatekeeper/src/flow.rs @@ -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, + 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; SLOTS], +} + /// The keystore's newest record, and where the next write goes. fn load(ks: &mut impl Keystore) -> Result<(Option, 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 { - // 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 { 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 { + // 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; 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 { 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" }); } } } diff --git a/crates/servers/gatekeeper/src/lib.rs b/crates/servers/gatekeeper/src/lib.rs index c1b1497..8b80c6d 100644 --- a/crates/servers/gatekeeper/src/lib.rs +++ b/crates/servers/gatekeeper/src/lib.rs @@ -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::(|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); diff --git a/crates/servers/gatekeeper/src/tests/mod.rs b/crates/servers/gatekeeper/src/tests/mod.rs index 98d215f..cb0e979 100644 --- a/crates/servers/gatekeeper/src/tests/mod.rs +++ b/crates/servers/gatekeeper/src/tests/mod.rs @@ -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 { vec![Block::default(); FAST.memory_kib as usize] } -fn run_with(dev: &mut Device, owner: &mut Owner) -> Result { - run(dev, &mut Counter(5), owner, FAST, &mut memory()).map(|u| u.key) +fn run_with(dev: &mut Device, owner: &mut Owner) -> Result { + 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"); } diff --git a/crates/types/src/gatekeeper.rs b/crates/types/src/gatekeeper.rs index 425410c..c1e6d0b 100644 --- a/crates/types/src/gatekeeper.rs +++ b/crates/types/src/gatekeeper.rs @@ -1,17 +1,34 @@ //! Init and the gatekeeper: after handing it the keystore and randomness -//! (device setup calls), init calls `UNLOCK`, which returns once the owner -//! is authenticated (`OK`, word 1 non-zero if the master key is new: a -//! fresh device or one just wiped) or the gatekeeper cannot go on -//! (`FAILED`); then `KEY`, whose reply is the 32-byte master key in its four -//! words, and `SEED`, 32 fresh random bytes from the hardware for storage's -//! randomness this boot. The gatekeeper then exits. Only the unbadged -//! holder (init) is answered. +//! (device setup calls), init calls `UNLOCK`, which returns once someone +//! is in (`OK`) or the gatekeeper cannot go on (`FAILED`). Then: +//! - `KEY`: the 32-byte master key of whoever signed in, in four words; +//! - `SEED`: 32 fresh random bytes from the hardware for storage this boot; +//! - `NAME`: their name, as a text message (`types::log`); +//! - `REMOVED`, until it says `DONE`: the id of a user removed just now, +//! whose files are to go. +//! +//! The gatekeeper then exits. Only the unbadged holder (init) is answered. pub const UNLOCK: usize = 1; pub const KEY: usize = 2; pub const SEED: usize = 3; +pub const NAME: usize = 4; +pub const REMOVED: usize = 5; + +/// `UNLOCK`'s reply: `[OK, flags, user id, role]`. +pub mod unlocked { + /// The keystore is new (first boot, or just wiped): no earlier user's + /// files can be opened any more. + pub const FRESH: usize = 1; + /// A guest: no user id, a key nobody keeps. + pub const GUEST: usize = 2; + pub const OWNER: usize = 1; + pub const STANDARD: usize = 2; +} pub mod reply { pub const OK: usize = 0; pub const FAILED: usize = 1; + /// `REMOVED` has no more. + pub const DONE: usize = 2; }