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:
@@ -1,23 +1,29 @@
|
|||||||
//! The keystore's contents (see the vision's Gatekeeper note): two sealed
|
//! The keystore's contents (see the vision's Gatekeeper and Users notes):
|
||||||
//! key slots, the failed-attempt counter, and the wipe state, in a record
|
//! eight sealed slots, the user directory, the failed-attempt counter and
|
||||||
//! kept twice so a torn write never loses both. Pure: the gatekeeper does
|
//! the wipe state, in a record kept twice so a torn write never loses
|
||||||
//! the I/O; the crypto is RustCrypto's (argon2id, ChaCha20-Poly1305,
|
//! both. Pure: the gatekeeper does the I/O; the crypto is RustCrypto's
|
||||||
//! BLAKE2s).
|
//! (argon2id, ChaCha20-Poly1305, BLAKE2s).
|
||||||
//!
|
//!
|
||||||
//! One slot seals the master key under the real passcode, the other a
|
//! A passcode is stretched once (argon2id, with the device's salt) and the
|
||||||
//! "wipe" marker under the duress passcode (or is plain random bytes when
|
//! result tried on every slot. A user's slot seals their master key, role,
|
||||||
//! there is none). Which is which is random, and both look like noise:
|
//! name and id (and, for an owner, the admin key that opens the
|
||||||
//! nothing on the device tells them apart, and every unlock tries both.
|
//! 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]
|
#![no_std]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod record;
|
mod record;
|
||||||
|
mod user;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
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 argon2::{Algorithm, Argon2, Params, Version};
|
||||||
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
|
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
|
||||||
@@ -27,17 +33,12 @@ use zeroize::Zeroize;
|
|||||||
pub use argon2::Block;
|
pub use argon2::Block;
|
||||||
|
|
||||||
pub const KEY_SIZE: usize = 32;
|
pub const KEY_SIZE: usize = 32;
|
||||||
const SALT_SIZE: usize = 16;
|
pub(crate) const SALT_SIZE: usize = 16;
|
||||||
const NONCE_SIZE: usize = 12;
|
pub(crate) const NONCE_SIZE: usize = 12;
|
||||||
const TAG_SIZE: usize = 16;
|
pub(crate) 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;
|
|
||||||
|
|
||||||
/// The master key, wiped from memory when dropped. Compared only in tests:
|
/// A key, wiped from memory when dropped. Compared only in tests: a plain
|
||||||
/// a plain `==` on secrets would not be constant-time.
|
/// `==` on secrets would not be constant-time.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
pub struct Key(pub [u8; KEY_SIZE]);
|
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.
|
/// Fills a buffer with randomness; false if it could not.
|
||||||
pub trait Random {
|
pub trait Random {
|
||||||
fn fill(&mut self, buf: &mut [u8]) -> bool;
|
fn fill(&mut self, buf: &mut [u8]) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// argon2id over the caller's memory (`params.memory_kib` blocks): no heap.
|
/// argon2id over the caller's memory (`params.memory_kib` blocks): no heap.
|
||||||
fn derive(passcode: &[u8], salt: &[u8], params: KdfParams, memory: &mut [Block]) -> Option<Key> {
|
pub(crate) fn stretch(
|
||||||
|
passcode: &[u8],
|
||||||
|
salt: &[u8],
|
||||||
|
params: KdfParams,
|
||||||
|
memory: &mut [Block],
|
||||||
|
) -> Option<Key> {
|
||||||
let p = Params::new(params.memory_kib, params.passes, 1, Some(KEY_SIZE)).ok()?;
|
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 memory = memory.get_mut(..params.memory_kib as usize)?;
|
||||||
let mut out = Key([0; KEY_SIZE]);
|
let mut out = Key([0; KEY_SIZE]);
|
||||||
@@ -79,146 +76,48 @@ fn derive(passcode: &[u8], salt: &[u8], params: KdfParams, memory: &mut [Block])
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A slot: salt, nonce, the sealed kind and key, the tag.
|
/// Seals `data` (in place) under `key` into `nonce | data | tag`.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
pub(crate) fn seal(key: &Key, out: &mut [u8], rng: &mut impl Random) -> bool {
|
||||||
pub struct Slot(pub [u8; SLOT_SIZE]);
|
let (nonce, rest) = out.split_at_mut(NONCE_SIZE);
|
||||||
|
let Some(split) = rest.len().checked_sub(TAG_SIZE) else {
|
||||||
impl Slot {
|
return false;
|
||||||
fn random(rng: &mut impl Random) -> Option<Self> {
|
};
|
||||||
let mut s = [0u8; SLOT_SIZE];
|
let (data, tag) = rest.split_at_mut(split);
|
||||||
rng.fill(&mut s).then_some(Self(s))
|
if !rng.fill(nonce) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
match ChaCha20Poly1305::new((&key.0).into()).encrypt_in_place_detached(
|
||||||
/// `kind` and `key`, sealed under a key derived from `passcode`.
|
Nonce::from_slice(nonce),
|
||||||
fn seal(
|
b"",
|
||||||
kind: u8,
|
data,
|
||||||
key: &Key,
|
) {
|
||||||
passcode: &[u8],
|
Ok(t) => {
|
||||||
params: KdfParams,
|
tag.copy_from_slice(&t);
|
||||||
memory: &mut [Block],
|
true
|
||||||
rng: &mut impl Random,
|
|
||||||
) -> Option<Self> {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
let kek = derive(passcode, salt, params, memory)?;
|
Err(_) => false,
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What the slot seals, if `passcode` opens it.
|
/// Opens what `seal` made into `plain` (its data's length); false (and
|
||||||
fn open(&self, passcode: &[u8], params: KdfParams, memory: &mut [Block]) -> Option<(u8, Key)> {
|
/// `plain` wiped) unless `key` sealed exactly this.
|
||||||
let (salt, rest) = self.0.split_at(SALT_SIZE);
|
pub(crate) fn open(key: &Key, sealed: &[u8], plain: &mut [u8]) -> bool {
|
||||||
let (nonce, rest) = rest.split_at(NONCE_SIZE);
|
let Some((nonce, rest)) = sealed.split_at_checked(NONCE_SIZE) else {
|
||||||
let (sealed, tag) = rest.split_at(SEALED_SIZE);
|
return false;
|
||||||
let kek = derive(passcode, salt, params, memory)?;
|
};
|
||||||
let mut plain = [0u8; SEALED_SIZE];
|
let Some(split) = rest.len().checked_sub(TAG_SIZE) else {
|
||||||
plain.copy_from_slice(sealed);
|
return false;
|
||||||
let opened = ChaCha20Poly1305::new((&kek.0).into())
|
};
|
||||||
.decrypt_in_place_detached(
|
let (data, tag) = rest.split_at(split);
|
||||||
Nonce::from_slice(nonce),
|
let Some(plain) = plain.get_mut(..data.len()) else {
|
||||||
b"",
|
return false;
|
||||||
&mut plain,
|
};
|
||||||
Tag::from_slice(tag),
|
plain.copy_from_slice(data);
|
||||||
)
|
let ok = ChaCha20Poly1305::new((&key.0).into())
|
||||||
.is_ok();
|
.decrypt_in_place_detached(Nonce::from_slice(nonce), b"", plain, Tag::from_slice(tag))
|
||||||
let mut key = Key([0; KEY_SIZE]);
|
.is_ok();
|
||||||
key.0.copy_from_slice(&plain[1..]);
|
if !ok {
|
||||||
let kind = plain[0];
|
|
||||||
plain.zeroize();
|
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<Self> {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
/// One record fills four sectors.
|
||||||
pub const RECORD_SIZE: usize = 512;
|
pub const RECORD_SIZE: usize = 2048;
|
||||||
const MAGIC: &[u8; 8] = b"KOSKEYS1";
|
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 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.
|
/// How slow a passcode guess is: argon2id's memory (KiB) and passes.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -32,18 +49,69 @@ pub enum State {
|
|||||||
Wiping,
|
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<Key>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Record {
|
pub struct Record {
|
||||||
/// Which copy is newer: the higher one.
|
/// Which copy is newer: the higher one.
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
pub state: State,
|
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,
|
pub threshold: u8,
|
||||||
/// Wrong passcodes in a row so far; counted before a passcode is
|
/// Wrong passcodes in a row so far; counted before a passcode is
|
||||||
/// checked, so pulling the power cannot save a guess.
|
/// checked, so pulling the power cannot save a guess.
|
||||||
pub failures: u8,
|
pub failures: u8,
|
||||||
|
/// A guest may sign in (with an empty name).
|
||||||
|
pub guests: bool,
|
||||||
pub params: KdfParams,
|
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 {
|
impl core::fmt::Debug for Record {
|
||||||
@@ -52,16 +120,412 @@ impl core::fmt::Debug for Record {
|
|||||||
.field("generation", &self.generation)
|
.field("generation", &self.generation)
|
||||||
.field("state", &self.state)
|
.field("state", &self.state)
|
||||||
.field("failures", &self.failures)
|
.field("failures", &self.failures)
|
||||||
.field("threshold", &self.threshold)
|
.field("guests", &self.guests)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checksum(body: &[u8]) -> [u8; CHECKSUM] {
|
fn noise<const N: usize>(rng: &mut impl Random) -> Option<[u8; N]> {
|
||||||
Blake2s256::digest(body).into()
|
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<User> {
|
||||||
|
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 {
|
impl Record {
|
||||||
|
fn name_tag(&self, name: &Name) -> [u8; NAME_TAG] {
|
||||||
|
let mut m = <Blake2sMac256 as KeyInit>::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<Entry>; 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<Entry>; 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<Key> {
|
||||||
|
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<Key> {
|
||||||
|
s.admin.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Who has an account (their duress slots left out); only for an owner.
|
||||||
|
pub fn users(&self, by: &Session) -> Result<[Option<User>; 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<Self, Refused> {
|
||||||
|
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<Self> {
|
||||||
|
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] {
|
pub fn encode(&self) -> [u8; RECORD_SIZE] {
|
||||||
let mut out = [0u8; RECORD_SIZE];
|
let mut out = [0u8; RECORD_SIZE];
|
||||||
out[0..8].copy_from_slice(MAGIC);
|
out[0..8].copy_from_slice(MAGIC);
|
||||||
@@ -72,11 +536,16 @@ impl Record {
|
|||||||
};
|
};
|
||||||
out[17] = self.threshold;
|
out[17] = self.threshold;
|
||||||
out[18] = self.failures;
|
out[18] = self.failures;
|
||||||
|
out[19] = u8::from(self.guests);
|
||||||
out[20..24].copy_from_slice(&self.params.memory_kib.to_le_bytes());
|
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[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..HEADER].copy_from_slice(&self.pepper);
|
||||||
out[28 + SLOT_SIZE..BODY].copy_from_slice(&self.slots[1].0);
|
out[HEADER..HEADER + DIRECTORY].copy_from_slice(&self.directory);
|
||||||
let sum = checksum(&out[..BODY]);
|
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[BODY..BODY + CHECKSUM].copy_from_slice(&sum);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -84,7 +553,8 @@ impl Record {
|
|||||||
/// `None` unless it is a whole, unaltered record (magic and checksum).
|
/// `None` unless it is a whole, unaltered record (magic and checksum).
|
||||||
pub fn decode(bytes: &[u8]) -> Option<Self> {
|
pub fn decode(bytes: &[u8]) -> Option<Self> {
|
||||||
let bytes = bytes.get(..RECORD_SIZE)?;
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap_or([0; 4]));
|
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,
|
2 => State::Wiping,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let mut a = [0u8; SLOT_SIZE];
|
let mut slots = [Slot([0; SLOT]); SLOTS];
|
||||||
let mut b = [0u8; SLOT_SIZE];
|
for (i, s) in slots.iter_mut().enumerate() {
|
||||||
a.copy_from_slice(&bytes[28..28 + SLOT_SIZE]);
|
let at = HEADER + DIRECTORY + i * SLOT;
|
||||||
b.copy_from_slice(&bytes[28 + SLOT_SIZE..BODY]);
|
s.0.copy_from_slice(&bytes[at..at + SLOT]);
|
||||||
|
}
|
||||||
Some(Self {
|
Some(Self {
|
||||||
generation: u64::from_le_bytes(bytes[8..16].try_into().ok()?),
|
generation: u64::from_le_bytes(bytes[8..16].try_into().ok()?),
|
||||||
state,
|
state,
|
||||||
threshold: bytes[17],
|
threshold: bytes[17],
|
||||||
failures: bytes[18],
|
failures: bytes[18],
|
||||||
params: crate::KdfParams {
|
guests: bytes[19] != 0,
|
||||||
|
params: KdfParams {
|
||||||
memory_kib: word(20),
|
memory_kib: word(20),
|
||||||
passes: word(24),
|
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)
|
self.newest().map_or(0, |(_, copy)| 1 - copy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _: () = assert!(BODY + CHECKSUM <= RECORD_SIZE);
|
||||||
|
|||||||
@@ -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<Block> {
|
fn memory() -> Vec<Block> {
|
||||||
vec![Block::default(); FAST.memory_kib as usize]
|
vec![Block::default(); FAST.memory_kib as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(duress: Option<&[u8]>, seed: u8) -> (Record, Key) {
|
fn name(s: &str) -> Name {
|
||||||
Record::setup(b"1234", duress, 5, FAST, &mut memory(), &mut Counter(seed)).unwrap()
|
Name::new(s.as_bytes()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn setup(duress: Option<&[u8]>, seed: u8) -> (Record, Session) {
|
||||||
fn the_real_passcode_opens_the_master_key_and_nothing_else_does() {
|
Record::setup(
|
||||||
let (record, master) = setup(Some(b"0000"), 1);
|
name("gabriel"),
|
||||||
let mem = &mut memory();
|
b"1234",
|
||||||
assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master));
|
duress,
|
||||||
assert_eq!(record.unlock(b"0000", mem), Outcome::Duress);
|
5,
|
||||||
for wrong in [&b"1235"[..], b"", b"12345", b"123"] {
|
FAST,
|
||||||
assert_eq!(record.unlock(wrong, mem), Outcome::Wrong, "{wrong:?}");
|
&mut memory(),
|
||||||
|
&mut Counter(seed),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signed_in(r: &Record, who: &str, passcode: &[u8]) -> Option<Session> {
|
||||||
|
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]
|
#[test]
|
||||||
fn without_a_duress_passcode_its_slot_opens_for_nothing() {
|
fn the_owner_signs_in_with_name_and_passcode() {
|
||||||
let (record, master) = setup(None, 2);
|
let (r, owner) = setup(Some(b"0000"), 1);
|
||||||
let mem = &mut memory();
|
assert!(owner.is_owner());
|
||||||
assert_eq!(record.unlock(b"1234", mem), Outcome::Real(master));
|
let again = signed_in(&r, "gabriel", b"1234").unwrap();
|
||||||
assert_eq!(record.unlock(b"0000", mem), Outcome::Wrong);
|
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!(
|
assert_eq!(
|
||||||
record.unlock(b"", mem),
|
r.add_user(
|
||||||
Outcome::Wrong,
|
&w,
|
||||||
"not even an empty one"
|
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]
|
#[test]
|
||||||
fn which_slot_is_real_is_not_fixed() {
|
fn the_last_owner_stays() {
|
||||||
// Different randomness puts the real slot first in some setups, second
|
let (r, owner) = setup(None, 4);
|
||||||
// in others: its position gives nothing away.
|
assert_eq!(
|
||||||
let mem = &mut memory();
|
r.remove_user(&owner, &name("gabriel"), &mut Counter(1))
|
||||||
let mut firsts = [false; 2];
|
.err(),
|
||||||
for seed in 0..16 {
|
Some(Refused::LastOwner)
|
||||||
let (record, _) = setup(Some(b"0000"), seed);
|
);
|
||||||
let only_first = Record {
|
}
|
||||||
slots: [record.slots[0], Slot(record.slots[1].0.map(|b| !b))],
|
|
||||||
..record
|
#[test]
|
||||||
};
|
fn slots_run_out() {
|
||||||
firsts[matches!(only_first.unlock(b"1234", mem), Outcome::Real(_)) as usize] = true;
|
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]
|
#[test]
|
||||||
fn records_round_trip_and_damage_is_detected() {
|
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();
|
let bytes = record.encode();
|
||||||
assert_eq!(Record::decode(&bytes), Some(record));
|
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;
|
let mut torn = bytes;
|
||||||
torn[at] ^= 1;
|
torn[at] ^= 1;
|
||||||
assert_eq!(Record::decode(&torn), None, "byte {at}");
|
assert_eq!(Record::decode(&torn), None, "byte {at}");
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(Record::decode(&[0; RECORD_SIZE]), None);
|
||||||
Record::decode(&[0; RECORD_SIZE]),
|
|
||||||
None,
|
|
||||||
"a blank device holds no record"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_newest_whole_copy_wins_and_writes_go_to_the_other() {
|
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 new = old.next(|r| r.failures = 1);
|
||||||
let (a, b) = (old.encode(), new.encode());
|
let (a, b) = (old.encode(), new.encode());
|
||||||
let copies = Copies { copies: [&a, &b] };
|
assert_eq!(Copies { copies: [&a, &b] }.newest(), Some((new, 1)));
|
||||||
assert_eq!(copies.newest(), Some((new, 1)));
|
assert_eq!(Copies { copies: [&b, &a] }.newest(), Some((new, 0)));
|
||||||
assert_eq!(copies.next_copy(), 0, "the older copy is overwritten");
|
assert_eq!(Copies { copies: [&a, &b] }.next_copy(), 0);
|
||||||
// A torn write of the newer copy leaves the older one.
|
|
||||||
let mut torn = b;
|
let mut torn = b;
|
||||||
torn[100] ^= 0xFF;
|
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!(
|
assert_eq!(
|
||||||
Copies {
|
Copies {
|
||||||
copies: [&blank, &blank]
|
copies: [&a, &torn]
|
||||||
}
|
}
|
||||||
.newest(),
|
.newest(),
|
||||||
None
|
Some((old, 0))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wiping_leaves_nothing_any_passcode_opens() {
|
fn wiping_leaves_nothing_any_name_opens() {
|
||||||
let (record, _) = setup(Some(b"0000"), 5);
|
let (record, owner) = setup(Some(b"0000"), 9);
|
||||||
let wiped = record.wiping(&mut Counter(99)).unwrap();
|
let wiped = record.wiping(&mut Counter(99)).unwrap();
|
||||||
assert_eq!(wiped.state, State::Wiping);
|
assert!(wiped.must_wipe());
|
||||||
assert!(
|
assert!(wiped.generation > record.generation);
|
||||||
wiped.must_wipe(),
|
|
||||||
"an interrupted wipe is finished next boot"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
wiped.generation > record.generation,
|
|
||||||
"it is the newest copy"
|
|
||||||
);
|
|
||||||
let mem = &mut memory();
|
|
||||||
for p in [&b"1234"[..], b"0000"] {
|
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]
|
#[test]
|
||||||
fn too_many_failures_mean_a_wipe() {
|
fn too_many_failures_mean_a_wipe() {
|
||||||
let (record, _) = setup(None, 6);
|
let (record, _) = setup(None, 10);
|
||||||
assert!(!record.must_wipe());
|
assert!(!record.must_wipe());
|
||||||
let worn = record.next(|r| r.failures = r.threshold);
|
assert!(record.next(|r| r.failures = r.threshold).must_wipe());
|
||||||
assert!(worn.must_wipe());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_randomness_means_no_keystore() {
|
fn names_are_printable_and_short() {
|
||||||
assert!(Record::setup(b"1234", None, 5, FAST, &mut memory(), &mut Broken).is_none());
|
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]
|
#[test]
|
||||||
fn the_default_parameters_are_accepted_by_argon2() {
|
fn user_ids_name_folders_in_hex() {
|
||||||
assert!(
|
assert_eq!(
|
||||||
argon2::Params::new(
|
&UserId([0xde, 0xad, 0xbe, 0xef, 0, 1, 2, 0xff]).hex(),
|
||||||
KdfParams::DEFAULT.memory_kib,
|
b"deadbeef000102ff"
|
||||||
KdfParams::DEFAULT.passes,
|
|
||||||
1,
|
|
||||||
Some(KEY_SIZE)
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
91
crates/keystore/src/user.rs
Normal file
91
crates/keystore/src/user.rs
Normal file
@@ -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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -9,16 +9,27 @@ use types::bounded::CapSlot;
|
|||||||
use crate::flow::{Keystore, MAX_SECRET, Prompt, Secret};
|
use crate::flow::{Keystore, MAX_SECRET, Prompt, Secret};
|
||||||
use keystore::RECORD_SIZE;
|
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);
|
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> {
|
impl<D: BlockDevice> Keystore for Disk<D> {
|
||||||
fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool {
|
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 {
|
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);
|
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 {
|
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||||
print_to(self.0, format_args!("{}", question));
|
print_to(self.0, format_args!("{}", question));
|
||||||
let mut line = Secret::new();
|
let mut line = Secret::new();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//! randomness, and the owner (`Prompt`). No kernel, no hardware: tested on
|
//! randomness, and the owner (`Prompt`). No kernel, no hardware: tested on
|
||||||
//! the host.
|
//! 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;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// The keystore device: the record's two copies.
|
/// 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
|
/// Shows `question` and reads a passcode without showing it; false if
|
||||||
/// nothing could be read.
|
/// nothing could be read.
|
||||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool;
|
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.
|
/// Why the gatekeeper could not hand over a key.
|
||||||
@@ -75,11 +77,23 @@ pub enum Failure {
|
|||||||
Prompt,
|
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;
|
pub const THRESHOLD: u8 = 10;
|
||||||
/// Shortest passcode accepted at setup.
|
/// Shortest passcode accepted.
|
||||||
pub const MIN_PASSCODE: usize = 4;
|
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.
|
/// The keystore's newest record, and where the next write goes.
|
||||||
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> {
|
fn load(ks: &mut impl Keystore) -> Result<(Option<Record>, usize), Failure> {
|
||||||
let (mut a, mut b) = ([0u8; RECORD_SIZE], [0u8; RECORD_SIZE]);
|
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) }
|
if ks.write(copy, &r.encode()) { Ok(()) } else { Err(Failure::Device) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Destroys the key slots: noise over both copies, twice, then blank (the
|
/// `r` written as the newest record.
|
||||||
/// device then looks never set up). Marked as wiping first, so a power cut
|
fn save(ks: &mut impl Keystore, r: &Record) -> Result<(), Failure> {
|
||||||
/// part way is finished on the next boot.
|
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> {
|
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 first = record.wiping(rng).ok_or(Failure::Random)?;
|
||||||
let second = first.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
|
/// A name, asked until it is a valid one.
|
||||||
/// may choose a duress one. Returns the new master key.
|
fn ask_name(prompt: &mut impl Prompt, question: &str) -> Result<Name, Failure> {
|
||||||
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();
|
|
||||||
loop {
|
loop {
|
||||||
choose(prompt, "Duress passcode: ", true, &mut duress)?;
|
let mut typed = Secret::new();
|
||||||
if duress.len == 0 || !duress.same(&real) {
|
if !prompt.line(question, &mut typed) {
|
||||||
break;
|
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");
|
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 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, 0, &record)?;
|
||||||
store(ks, 1, &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
|
/// What the owner may change at the lock screen, after signing in.
|
||||||
/// device, or one just wiped), so nothing sealed under an old one exists.
|
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> {
|
||||||
pub struct Unlocked {
|
loop {
|
||||||
pub key: Key,
|
let (Some(record), _) = load(ks)? else { return Err(Failure::Device) };
|
||||||
pub fresh: bool,
|
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
|
/// Authenticates someone and returns their key: setting up a fresh
|
||||||
/// a fresh device, wiping on duress or too many failures (the device then
|
/// device's owner, wiping everyone on a duress passcode or too many
|
||||||
/// looks fresh, and is set up anew).
|
/// 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> {
|
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
|
||||||
loop {
|
loop {
|
||||||
let (record, next) = load(ks)?;
|
let (record, next) = load(ks)?;
|
||||||
let Some(record) = record else {
|
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() {
|
if record.must_wipe() {
|
||||||
wipe(ks, &record, next, rng)?;
|
wipe(ks, &record, next, rng)?;
|
||||||
continue;
|
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();
|
let mut typed = Secret::new();
|
||||||
if !prompt.secret("Passcode: ", &mut typed) {
|
if !prompt.secret("Passcode: ", &mut typed) {
|
||||||
return Err(Failure::Prompt);
|
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.
|
// Counted before it is checked: pulling the power cannot save a guess.
|
||||||
let counted = record.next(|r| r.failures = r.failures.saturating_add(1));
|
let counted = record.next(|r| r.failures = r.failures.saturating_add(1));
|
||||||
store(ks, next, &counted)?;
|
store(ks, next, &counted)?;
|
||||||
match record.unlock(typed.as_bytes(), memory) {
|
match record.unlock(name.as_bytes(), typed.as_bytes(), memory) {
|
||||||
Outcome::Real(key) => {
|
Outcome::User(session) => {
|
||||||
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
|
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 => {
|
Outcome::Duress => {
|
||||||
// Recorded as worn out first: a wipe cut short resumes.
|
// 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 => {
|
Outcome::Wrong => {
|
||||||
let left = counted.threshold.saturating_sub(counted.failures);
|
let left = counted.threshold.saturating_sub(counted.failures);
|
||||||
if left > 0 {
|
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 keystore::{Block, KdfParams};
|
||||||
use libkos::syscall::{self, Received};
|
use libkos::syscall::{self, Received};
|
||||||
use types::bounded::CapSlot;
|
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
|
/// Where init's manifest put them: its own endpoint (devices arrive on
|
||||||
/// it, then init's calls), then the terminal.
|
/// it, then init's calls), then the terminal.
|
||||||
@@ -67,18 +67,48 @@ pub fn run() -> ! {
|
|||||||
}
|
}
|
||||||
drop(memory);
|
drop(memory);
|
||||||
match unlocked {
|
match unlocked {
|
||||||
Ok(flow::Unlocked { key, fresh }) => {
|
Ok(flow::Unlocked { user, key, fresh, removed }) => {
|
||||||
libkos::println!("gatekeeper: unlocked");
|
libkos::println!("gatekeeper: unlocked");
|
||||||
libkos::terminal::clear(TERMINAL);
|
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 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 mut seed = [0u8; 32];
|
||||||
let seeded = keystore::Random::fill(&mut random, &mut seed);
|
if !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 {
|
|
||||||
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
|
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);
|
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) => {
|
Err(why) => {
|
||||||
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
extern crate std;
|
extern crate std;
|
||||||
|
|
||||||
use crate::flow::*;
|
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::collections::VecDeque;
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
@@ -90,144 +90,160 @@ impl Prompt for Owner {
|
|||||||
}
|
}
|
||||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||||
self.shown.push_str(question);
|
self.shown.push_str(question);
|
||||||
|
self.said.push_str(question);
|
||||||
let Some(line) = self.typing.pop_front() else { return false };
|
let Some(line) = self.typing.pop_front() else { return false };
|
||||||
out.set(line.as_bytes());
|
out.set(line.as_bytes());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
fn line(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||||
|
self.secret(question, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn memory() -> Vec<Block> {
|
fn memory() -> Vec<Block> {
|
||||||
vec![Block::default(); FAST.memory_kib as usize]
|
vec![Block::default(); FAST.memory_kib as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<keystore::Key, Failure> {
|
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<Unlocked, Failure> {
|
||||||
run(dev, &mut Counter(5), owner, FAST, &mut memory()).map(|u| u.key)
|
run(dev, &mut Counter(5), owner, FAST, &mut memory())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fresh(dev: &mut Device, owner: &mut Owner) -> bool {
|
fn key_of(dev: &mut Device, lines: &[&'static str]) -> Option<[u8; 32]> {
|
||||||
run(dev, &mut Counter(5), owner, FAST, &mut memory()).unwrap().fresh
|
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]) {
|
fn set_up() -> (Device, [u8; 32]) {
|
||||||
let mut dev = Device::new();
|
let mut dev = Device::new();
|
||||||
let key = run_with(&mut dev, &mut Owner::types(&["1234", "1234", "0000", "0000"])).unwrap();
|
let u = run_with(&mut dev, &mut Owner::types(&["gabriel", "1234", "1234", "0000", "0000"])).unwrap();
|
||||||
(dev, key.0)
|
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]
|
#[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 dev, key) = set_up();
|
||||||
let mut owner = Owner::types(&["1234"]);
|
let mut owner = Owner::types(&SIGN_IN);
|
||||||
assert_eq!(run_with(&mut dev, &mut owner).unwrap().0, key, "the same master key every boot");
|
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);
|
assert_eq!(dev.record().unwrap().failures, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn setup_insists_on_a_confirmed_passcode_of_four_or_more() {
|
fn a_wrong_name_and_a_wrong_passcode_look_alike_and_both_count() {
|
||||||
let mut dev = Device::new();
|
let (mut dev, key) = set_up();
|
||||||
let mut owner = Owner::types(&["1234", "1243", "12", "12", "123456", "123456", "", ""]);
|
let mut owner = Owner::types(&["nobody", "1234", "gabriel", "9999", "gabriel", "1234", ""]);
|
||||||
run_with(&mut dev, &mut owner).unwrap();
|
assert_eq!(run_with(&mut dev, &mut owner).unwrap().key.0, key);
|
||||||
assert!(owner.shown.contains("They differ"));
|
assert_eq!(owner.said.matches("Wrong name or passcode.").count(), 2);
|
||||||
assert!(owner.shown.contains("At least 4"));
|
|
||||||
let mut unlock = Owner::types(&["123456"]);
|
|
||||||
assert!(run_with(&mut dev, &mut unlock).is_ok(), "no duress chosen: the passcode is 123456");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn the_duress_passcode_must_differ() {
|
|
||||||
let mut dev = Device::new();
|
|
||||||
let mut owner = Owner::types(&["1234", "1234", "1234", "1234", "", ""]);
|
|
||||||
run_with(&mut dev, &mut owner).unwrap();
|
|
||||||
assert!(owner.shown.contains("must differ"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_guess_is_counted_before_it_is_checked() {
|
fn every_guess_is_counted_before_it_is_checked() {
|
||||||
let (mut dev, _) = set_up();
|
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);
|
dev.cut_after = Some(dev.writes + 1);
|
||||||
let mut owner = Owner::types(&["1234"]);
|
assert_eq!(run_with(&mut dev, &mut Owner::types(&SIGN_IN)).err(), Some(Failure::Device));
|
||||||
assert_eq!(run_with(&mut dev, &mut owner).err(), Some(Failure::Device));
|
|
||||||
assert_eq!(dev.record().unwrap().failures, 1, "the guess stays counted");
|
assert_eq!(dev.record().unwrap().failures, 1, "the guess stays counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 dev, key) = set_up();
|
||||||
let mut owner = Owner::types(&["1111", "2222", "1234"]);
|
let mut lines: Vec<&'static str> = Vec::new();
|
||||||
assert_eq!(run_with(&mut dev, &mut owner).unwrap().0, key);
|
for _ in 0..THRESHOLD {
|
||||||
assert_eq!(owner.shown.matches("Wrong passcode").count(), 2);
|
lines.extend(["gabriel", "9999"]);
|
||||||
assert_eq!(dev.record().unwrap().failures, 0);
|
}
|
||||||
}
|
lines.extend(["anna", "5678", "5678", "", ""]);
|
||||||
|
let mut owner = Owner::types(&lines);
|
||||||
#[test]
|
let u = run_with(&mut dev, &mut owner).unwrap();
|
||||||
fn too_many_wrong_passcodes_wipe_the_keys_and_the_device_starts_fresh() {
|
|
||||||
let (mut dev, key) = set_up();
|
|
||||||
let mut guesses = vec!["9999"; THRESHOLD as usize];
|
|
||||||
// Then it looks fresh: the owner sets it up again.
|
|
||||||
guesses.extend(["5678", "5678", "", ""]);
|
|
||||||
let mut owner = Owner::types(&guesses);
|
|
||||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
|
||||||
assert!(owner.said.contains("One try left"));
|
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!(u.fresh);
|
||||||
assert_ne!(new.0, key, "the old master key is gone");
|
assert_ne!(u.key.0, key);
|
||||||
let mut old = Owner::types(&["1234"]);
|
assert!(owner.shown.starts_with("This device has no users yet"), "nothing earlier left on the screen");
|
||||||
let mut dev2 = dev.clone();
|
|
||||||
dev2.cut_after = None;
|
|
||||||
assert!(run_with(&mut dev2, &mut old).is_err(), "the old passcode opens nothing");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 dev, key) = set_up();
|
||||||
let mut owner = Owner::types(&["0000", "4321", "4321", "", ""]);
|
let mut owner = Owner::types(&["gabriel", "0000", "anna", "4321", "4321", "", ""]);
|
||||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
let u = run_with(&mut dev, &mut owner).unwrap();
|
||||||
assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode");
|
assert!(!owner.said.contains("Wrong"));
|
||||||
assert!(owner.shown.starts_with("This device has no passcode yet"), "nor is the passcode prompt left showing");
|
assert!(owner.shown.starts_with("This device has no users yet"));
|
||||||
assert!(owner.shown.contains("no passcode yet"));
|
assert!(u.fresh);
|
||||||
assert_ne!(new.0, key);
|
assert_ne!(u.key.0, key);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_wipe_cut_short_is_finished_on_the_next_boot() {
|
|
||||||
let (mut dev, key) = set_up();
|
|
||||||
// Duress, and the power goes during the wipe (after its first write).
|
|
||||||
let marked = dev.writes + 2;
|
|
||||||
dev.cut_after = Some(marked + 1);
|
|
||||||
assert_eq!(run_with(&mut dev, &mut Owner::types(&["0000"])).err(), Some(Failure::Device));
|
|
||||||
dev.cut_after = None;
|
|
||||||
// Next boot: the wipe completes before anything is asked; then setup.
|
|
||||||
let mut owner = Owner::types(&["4321", "4321", "", ""]);
|
|
||||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
|
||||||
assert!(owner.shown.starts_with("This device has no passcode yet"), "no passcode is asked of a half-wiped device");
|
|
||||||
assert_ne!(new.0, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_duress_cut_short_before_the_wipe_still_wipes_next_boot() {
|
|
||||||
let (mut dev, _) = set_up();
|
|
||||||
// The power goes right after the duress was recorded (before any wipe write).
|
|
||||||
dev.cut_after = Some(dev.writes + 2);
|
|
||||||
assert_eq!(run_with(&mut dev, &mut Owner::types(&["0000"])).err(), Some(Failure::Device));
|
|
||||||
assert!(dev.record().unwrap().must_wipe());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_duress_whose_wipe_is_torn_still_wipes_next_boot() {
|
fn a_duress_whose_wipe_is_torn_still_wipes_next_boot() {
|
||||||
let (mut dev, _) = set_up();
|
let (mut dev, _) = set_up();
|
||||||
// Counted, recorded, then the wipe's first write is torn.
|
|
||||||
dev.tear_wipe = true;
|
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;
|
dev.cut_after = None;
|
||||||
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
|
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_a_new_master_key_counts_as_fresh() {
|
fn a_wipe_cut_short_is_finished_on_the_next_boot() {
|
||||||
let mut dev = Device::new();
|
let (mut dev, key) = set_up();
|
||||||
assert!(fresh(&mut dev, &mut Owner::types(&["1234", "1234", "0000", "0000"])), "first boot");
|
dev.cut_after = Some(dev.writes + 3);
|
||||||
assert!(!fresh(&mut dev, &mut Owner::types(&["1234"])), "an unlock");
|
assert_eq!(run_with(&mut dev, &mut Owner::types(&["gabriel", "0000"])).err(), Some(Failure::Device));
|
||||||
assert!(fresh(&mut dev, &mut Owner::types(&["0000", "5678", "5678", "", ""])), "after a duress wipe");
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
//! Init and the gatekeeper: after handing it the keystore and randomness
|
//! Init and the gatekeeper: after handing it the keystore and randomness
|
||||||
//! (device setup calls), init calls `UNLOCK`, which returns once the owner
|
//! (device setup calls), init calls `UNLOCK`, which returns once someone
|
||||||
//! is authenticated (`OK`, word 1 non-zero if the master key is new: a
|
//! is in (`OK`) or the gatekeeper cannot go on (`FAILED`). Then:
|
||||||
//! fresh device or one just wiped) or the gatekeeper cannot go on
|
//! - `KEY`: the 32-byte master key of whoever signed in, in four words;
|
||||||
//! (`FAILED`); then `KEY`, whose reply is the 32-byte master key in its four
|
//! - `SEED`: 32 fresh random bytes from the hardware for storage this boot;
|
||||||
//! words, and `SEED`, 32 fresh random bytes from the hardware for storage's
|
//! - `NAME`: their name, as a text message (`types::log`);
|
||||||
//! randomness this boot. The gatekeeper then exits. Only the unbadged
|
//! - `REMOVED`, until it says `DONE`: the id of a user removed just now,
|
||||||
//! holder (init) is answered.
|
//! whose files are to go.
|
||||||
|
//!
|
||||||
|
//! The gatekeeper then exits. Only the unbadged holder (init) is answered.
|
||||||
|
|
||||||
pub const UNLOCK: usize = 1;
|
pub const UNLOCK: usize = 1;
|
||||||
pub const KEY: usize = 2;
|
pub const KEY: usize = 2;
|
||||||
pub const SEED: usize = 3;
|
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 mod reply {
|
||||||
pub const OK: usize = 0;
|
pub const OK: usize = 0;
|
||||||
pub const FAILED: usize = 1;
|
pub const FAILED: usize = 1;
|
||||||
|
/// `REMOVED` has no more.
|
||||||
|
pub const DONE: usize = 2;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user