feat(storage): per-app/owner/system encryption live: gatekeeper hands seed + fresh flag, storage unlocks layer, init seeds areas from factory/ (empties on new key); rmdir op; test-qemu checks sealed home + reboot persistence

This commit is contained in:
2026-10-28 00:07:41 +01:00
parent e8191be160
commit 9dc751c466
20 changed files with 344 additions and 72 deletions

View File

@@ -151,14 +151,21 @@ fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt
Ok(key)
}
/// 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,
}
/// Authenticates the owner and returns the master key: setting one up on
/// a fresh device, wiping on duress or too many failures (the device then
/// looks fresh, and is set up anew).
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
loop {
let (record, next) = load(ks)?;
let Some(record) = record else {
return setup(ks, rng, prompt, params, memory);
return setup(ks, rng, prompt, params, memory).map(|key| Unlocked { key, fresh: true });
};
if record.must_wipe() {
wipe(ks, &record, next, rng)?;
@@ -174,7 +181,7 @@ pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prom
match record.unlock(typed.as_bytes(), memory) {
Outcome::Real(key) => {
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
return Ok(key);
return Ok(Unlocked { key, fresh: false });
}
Outcome::Duress => {
// Recorded as worn out first: a wipe cut short resumes.

View File

@@ -18,7 +18,7 @@ use alloc::vec;
use keystore::{Block, KdfParams};
use libkos::syscall::{self, Received};
use types::bounded::CapSlot;
use types::gatekeeper::{KEY, UNLOCK, reply};
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
/// Where init's manifest put them: its own endpoint (devices arrive on
/// it, then init's calls), then the terminal.
@@ -53,9 +53,10 @@ pub fn run() -> ! {
};
let mut memory = vec![Block::default(); KdfParams::DEFAULT.memory_kib as usize];
libkos::println!("gatekeeper: waiting for the owner");
let key = flow::run(
let mut random = devices::Hardware(random);
let unlocked = flow::run(
&mut devices::Disk(keystore),
&mut devices::Hardware(random),
&mut random,
&mut devices::Terminal(TERMINAL),
KdfParams::DEFAULT,
&mut memory,
@@ -65,14 +66,19 @@ pub fn run() -> ! {
*b = Block::default();
}
drop(memory);
match key {
Ok(key) => {
match unlocked {
Ok(flow::Unlocked { key, fresh }) => {
libkos::println!("gatekeeper: unlocked");
libkos::terminal::clear(TERMINAL);
let word = |i: usize| usize::from_le_bytes(key.0[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8]));
if answer(UNLOCK, [reply::OK, 0, 0, 0]) {
let _ = answer(KEY, [word(0), word(1), word(2), word(3)]);
let words = |b: &[u8; 32]| core::array::from_fn::<usize, 4, _>(|i| usize::from_le_bytes(b[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])));
let mut seed = [0u8; 32];
let seeded = keystore::Random::fill(&mut random, &mut seed);
if seeded && answer(UNLOCK, [reply::OK, usize::from(fresh), 0, 0]) && answer(KEY, words(&key.0)) {
let _ = answer(SEED, words(&seed));
} else if !seeded {
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
}
seed.fill(0);
}
Err(why) => {
libkos::println!("gatekeeper: cannot go on: {:?}", why);

View File

@@ -101,7 +101,11 @@ fn memory() -> Vec<Block> {
}
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<keystore::Key, Failure> {
run(dev, &mut Counter(5), owner, FAST, &mut memory())
run(dev, &mut Counter(5), owner, FAST, &mut memory()).map(|u| u.key)
}
fn fresh(dev: &mut Device, owner: &mut Owner) -> bool {
run(dev, &mut Counter(5), owner, FAST, &mut memory()).unwrap().fresh
}
/// A device set up with passcode 1234 and duress 0000; its master key.
@@ -219,3 +223,11 @@ fn a_duress_whose_wipe_is_torn_still_wipes_next_boot() {
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");
}