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

137
crates/init/src/factory.rs Normal file
View File

@@ -0,0 +1,137 @@
//! The encrypted areas' starting point. The image ships starter content
//! (the owner's first files, the pre-approved decisions) in plaintext under
//! `factory/`; with a new master key (a fresh device, or one just wiped)
//! init empties the encrypted areas and seeds them from it, storage
//! sealing what is written. A device just wiped so looks factory-fresh,
//! not full of files no key opens. An area simply missing (a disk image
//! rebuilt) is seeded too.
use libkos::fs::Storage;
use types::storage::MAX_PATH;
const AREAS: [&str; 3] = ["home", "system", "data"];
/// Entries handled per listing; a folder with more is listed again.
const BATCH: usize = 32;
const NAME: usize = types::storage::DIR_NAME_SIZE;
/// A path being built, `a/b/c`.
#[derive(Clone, Copy)]
struct Path {
bytes: [u8; MAX_PATH],
len: usize,
}
impl Path {
fn new(s: &str) -> Self {
let mut p = Self { bytes: [0; MAX_PATH], len: 0 };
p.bytes[..s.len()].copy_from_slice(s.as_bytes());
p.len = s.len();
p
}
fn join(&self, name: &[u8]) -> Option<Self> {
let mut p = *self;
let end = p.len + 1 + name.len();
let room = p.bytes.get_mut(p.len..end)?;
room[0] = b'/';
room[1..].copy_from_slice(name);
p.len = end;
Some(p)
}
fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len]
}
}
/// Up to `BATCH` entries of `dir`: names and whether each is a folder.
fn entries(fs: &mut Storage, dir: &Path) -> Option<([([u8; NAME], usize, bool); BATCH], usize)> {
let mut out = [([0u8; NAME], 0, false); BATCH];
let mut n = 0;
fs.list_dir(dir.as_bytes(), |e| {
let name = e.name();
if name != b"." && name != b".." && n < BATCH {
out[n].0[..name.len()].copy_from_slice(name);
out[n].1 = name.len();
out[n].2 = e.is_dir;
n += 1;
}
})
.ok()?;
Some((out, n))
}
/// Removes everything inside `dir`, keeping `dir`.
fn empty(fs: &mut Storage, dir: &Path) -> Result<(), &'static str> {
loop {
let (list, n) = entries(fs, dir).ok_or("cannot list an area to empty it")?;
if n == 0 {
return Ok(());
}
for (name, len, is_dir) in &list[..n] {
let child = dir.join(&name[..*len]).ok_or("path too long")?;
if *is_dir {
empty(fs, &child)?;
fs.rmdir(child.as_bytes()).map_err(|_| "cannot remove a folder")?;
} else {
fs.delete(child.as_bytes()).map_err(|_| "cannot remove a file")?;
}
}
}
}
/// Copies the file `from` to a new file `to`.
fn copy_file(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
let src = fs.open(from.as_bytes()).map_err(|_| "cannot open a factory file")?;
let dst = fs.create(to.as_bytes()).map_err(|_| "cannot create a file")?;
let done = loop {
match fs.read(src, buf) {
Ok(0) => break Ok(()),
Ok(n) if fs.write(dst, &buf[..n]) == Ok(n) => {}
_ => break Err("cannot copy a factory file"),
}
};
let _ = fs.close(src);
let _ = fs.close(dst);
done
}
/// Copies everything in `from` into `to` (which exists).
fn copy_tree(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
let (list, n) = match entries(fs, from) {
Some(l) => l,
// Nothing shipped for this area.
None => return Ok(()),
};
for (name, len, is_dir) in &list[..n] {
let (src, dst) = (from.join(&name[..*len]).ok_or("path too long")?, to.join(&name[..*len]).ok_or("path too long")?);
if *is_dir {
fs.mkdir(dst.as_bytes()).map_err(|_| "cannot make a folder")?;
copy_tree(fs, &src, &dst, buf)?;
} else {
copy_file(fs, &src, &dst, buf)?;
}
}
Ok(())
}
/// Readies the encrypted areas once storage has the key: `fresh` when the
/// master key is new.
pub(crate) fn prepare(fs: &mut Storage, fresh: bool, buf: &mut [u8]) -> Result<(), &'static str> {
for area in AREAS {
let path = Path::new(area);
let present = fs.list_dir(path.as_bytes(), |_| {}).is_ok();
if present && !fresh {
continue;
}
if present {
empty(fs, &path)?;
} else {
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
}
let factory = Path::new("factory").join(area.as_bytes()).ok_or("path too long")?;
copy_tree(fs, &factory, &path, buf)?;
libkos::println!("init: {} {}", if fresh { "set up" } else { "seeded" }, area);
}
Ok(())
}

View File

@@ -114,8 +114,9 @@ impl<'a> Launcher<'a> {
note = self.give(*device, ep);
}
if p.unlocks {
unlock(ep)?;
note = ", the owner is in";
let fresh = unlock(ep)?;
crate::factory::prepare(self.storage, fresh, self.scratch)?;
note = if fresh { ", the owner is in (a new key)" } else { ", the owner is in" };
}
if console.is_some() {
hand_over_console(ep);
@@ -154,20 +155,26 @@ impl<'a> Launcher<'a> {
}
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
/// passes the master key on to storage, which alone keeps it.
fn unlock(ep: CapSlot) -> Result<(), &'static str> {
use types::gatekeeper::{KEY, UNLOCK, reply};
match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
Ok([reply::OK, ..]) => {}
/// passes the master key and a fresh seed on to storage, which alone keeps
/// them. Whether the key is new.
fn unlock(ep: CapSlot) -> Result<bool, &'static str> {
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
let fresh = match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
Ok([reply::OK, fresh, ..]) => fresh != 0,
_ => return Err("the gatekeeper could not unlock"),
};
for (ask, give) in [(KEY, types::storage::op::SET_KEY), (SEED, types::storage::op::SET_SEED)] {
let mut secret = syscall::request(ep, [ask, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
let given = [0, 1].iter().all(|&half| {
let words = [give, half, secret[half * 2], secret[half * 2 + 1]];
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
});
secret.fill(0);
if !given {
return Err("storage would not take the key");
}
}
let mut key = syscall::request(ep, [KEY, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
let given = [0, 1].iter().all(|&half| {
let words = [types::storage::op::SET_KEY, half, key[half * 2], key[half * 2 + 1]];
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
});
key.fill(0);
if given { Ok(()) } else { Err("storage would not take the key") }
Ok(fresh)
}
/// From here on init prints through the log server, like everyone else,

View File

@@ -4,6 +4,7 @@
extern crate alloc;
mod devices;
mod factory;
mod launch;
mod load;
mod manifest;