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:
137
crates/init/src/factory.rs
Normal file
137
crates/init/src/factory.rs
Normal 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(())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
extern crate alloc;
|
||||
|
||||
mod devices;
|
||||
mod factory;
|
||||
mod launch;
|
||||
mod load;
|
||||
mod manifest;
|
||||
|
||||
Reference in New Issue
Block a user