diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index 8cb4476..63b634b 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -125,10 +125,46 @@ fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> { Ok(()) } +/// The owner's area is sealed: seeded from the factory, and what is +/// written there one boot reads back the next. +fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> { + let mut home = [0u8; 512]; + let mut factory = [0u8; 512]; + let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result { + let h = fs.open(path).map_err(|_| "cannot open")?; + let n = fs.read(h, out).map_err(|_| "cannot read (a seal broken?)")?; + let _ = fs.close(h); + Ok(n) + }; + let n = read(fs, b"home/readme.txt", &mut home)?; + let m = read(fs, b"factory/home/readme.txt", &mut factory)?; + if home[..n] != factory[..m] { + return Err("the encrypted home does not hold the factory's readme"); + } + libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok"); + const NOTE: &[u8] = b"sealed across a reboot"; + let mut back = [0u8; 64]; + match read(fs, b"home/persist.txt", &mut back) { + Ok(n) if &back[..n] == NOTE => libkos::println!("spawntest: an encrypted file from the last boot reads back: ok"), + Ok(_) => return Err("an encrypted file from the last boot reads back wrong"), + Err(_) => { + let h = fs.create(b"home/persist.txt").map_err(|_| "cannot create in home")?; + let wrote = fs.write(h, NOTE) == Ok(NOTE.len()); + let _ = fs.close(h); + if !wrote { + return Err("cannot write in home"); + } + libkos::println!("spawntest: wrote an encrypted file for the next boot"); + } + } + Ok(()) +} + fn check() -> Result<(), &'static str> { let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?; write_a_file(&mut fs)?; libkos::println!("spawntest: a file written to ext4 reads back: ok"); + encrypted_home(&mut fs)?; let died = syscall::create_notification().map_err(|_| "no notification")?; narrow_storage(&mut fs, died)?; diff --git a/crates/init/src/factory.rs b/crates/init/src/factory.rs new file mode 100644 index 0000000..33d0d3d --- /dev/null +++ b/crates/init/src/factory.rs @@ -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 { + 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(()) +} diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 823a0ae..02ddc4a 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -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 { + 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, diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 2fd98bc..b8d72b3 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; mod devices; +mod factory; mod launch; mod load; mod manifest; diff --git a/crates/libkos/src/fs.rs b/crates/libkos/src/fs.rs index 48978e8..23518d5 100644 --- a/crates/libkos/src/fs.rs +++ b/crates/libkos/src/fs.rs @@ -136,6 +136,11 @@ impl Storage { self.with_path(op::MKDIR, path, reply::OK).map(|_| ()) } + /// Removes the empty directory at `path`. + pub fn rmdir(&mut self, path: &[u8]) -> Result<()> { + self.with_path(op::RMDIR, path, reply::OK).map(|_| ()) + } + pub fn close(&mut self, handle: FileHandle) -> Result<()> { self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ()) } diff --git a/crates/servers/gatekeeper/src/flow.rs b/crates/servers/gatekeeper/src/flow.rs index 87655ae..b61a1ec 100644 --- a/crates/servers/gatekeeper/src/flow.rs +++ b/crates/servers/gatekeeper/src/flow.rs @@ -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 { +pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result { loop { let (record, next) = load(ks)?; let Some(record) = record else { - return setup(ks, rng, prompt, params, memory); + 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. diff --git a/crates/servers/gatekeeper/src/lib.rs b/crates/servers/gatekeeper/src/lib.rs index a1bc2c9..c1b1497 100644 --- a/crates/servers/gatekeeper/src/lib.rs +++ b/crates/servers/gatekeeper/src/lib.rs @@ -18,7 +18,7 @@ use alloc::vec; use keystore::{Block, KdfParams}; use libkos::syscall::{self, Received}; use types::bounded::CapSlot; -use types::gatekeeper::{KEY, 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::(|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); diff --git a/crates/servers/gatekeeper/src/tests/mod.rs b/crates/servers/gatekeeper/src/tests/mod.rs index 2032dfe..98d215f 100644 --- a/crates/servers/gatekeeper/src/tests/mod.rs +++ b/crates/servers/gatekeeper/src/tests/mod.rs @@ -101,7 +101,11 @@ fn memory() -> Vec { } fn run_with(dev: &mut Device, owner: &mut Owner) -> Result { - 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"); +} diff --git a/crates/servers/storage-server/src/lib.rs b/crates/servers/storage-server/src/lib.rs index 19c26ef..a5fe92a 100644 --- a/crates/servers/storage-server/src/lib.rs +++ b/crates/servers/storage-server/src/lib.rs @@ -76,6 +76,6 @@ pub fn run() -> ! { libkos::println!("storage: no ext4 filesystem it can read, exiting"); syscall::exit(); }; - let mut srv = server::StorageServer::new(ext4, Kernel); + let mut srv = server::StorageServer::new(encrypted::Encrypted::new(ext4), Kernel); server_framework::serve(&mut srv, ep) } diff --git a/crates/servers/storage-server/src/protocol.rs b/crates/servers/storage-server/src/protocol.rs index c0f790d..d2affc4 100644 --- a/crates/servers/storage-server/src/protocol.rs +++ b/crates/servers/storage-server/src/protocol.rs @@ -27,6 +27,9 @@ pub enum StorageRequest { SetLog, /// Half (`half`: 0 or 1) of the master key. SetKey { half: usize, words: [usize; 2] }, + /// Half of the seed for randomness. + SetSeed { half: usize, words: [usize; 2] }, + Rmdir { path_len: usize }, /// A capability to the path (`path_len` bytes of the buffer), inside /// the caller's scope. Grant { @@ -77,6 +80,8 @@ impl Encode for StorageRequest { Mkdir { path_len } => [op::MKDIR, path_len, 0, 0], SetLog => [op::SET_LOG, 0, 0, 0], SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]], + SetSeed { half, words } => [op::SET_SEED, half, words[0], words[1]], + Rmdir { path_len } => [op::RMDIR, path_len, 0, 0], Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0], Release => [op::RELEASE, 0, 0, 0], } @@ -99,6 +104,8 @@ impl Decode for StorageRequest { op::MKDIR => Mkdir { path_len: w[1] }, op::SET_LOG => SetLog, op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] }, + op::SET_SEED => SetSeed { half: w[1], words: [w[2], w[3]] }, + op::RMDIR => Rmdir { path_len: w[1] }, op::GRANT => Grant { path_len: w[1], write: w[2] != 0, diff --git a/crates/servers/storage-server/src/server.rs b/crates/servers/storage-server/src/server.rs index d081533..d549e75 100644 --- a/crates/servers/storage-server/src/server.rs +++ b/crates/servers/storage-server/src/server.rs @@ -50,9 +50,11 @@ pub struct StorageServer { clients: [Option; MAX_CLIENTS], owners: [Option; MAX_HANDLES], log: Option, - /// The master key's halves, from the gatekeeper through init: what the - /// disk's encryption will be keyed with. + /// The master key's and the seed's halves, from the gatekeeper through + /// init; handed to the file system (and forgotten here) once all in. key: [Option<[usize; 2]>; 2], + seed: [Option<[usize; 2]>; 2], + unlocked: bool, } impl StorageServer { @@ -65,6 +67,8 @@ impl StorageServer { owners: [None; MAX_HANDLES], log: None, key: [None; 2], + seed: [None; 2], + unlocked: false, } } @@ -88,20 +92,29 @@ impl StorageServer { } /// Only the unbadged holder (init), each half only once: nobody can - /// replace the key later. - fn set_key(&mut self, badge: usize, half: usize, words: [usize; 2]) -> StorageResponse { - match (badge, self.key.get_mut(half)) { - (0, Some(slot @ None)) => { - *slot = Some(words); - StorageResponse::Ok - } - _ => StorageResponse::Error, + /// replace the key or the seed later. + fn set_half(&mut self, badge: usize, seed: bool, half: usize, words: [usize; 2]) -> StorageResponse { + let halves = if seed { &mut self.seed } else { &mut self.key }; + match (badge, self.unlocked, halves.get_mut(half)) { + (0, false, Some(slot @ None)) => *slot = Some(words), + _ => return StorageResponse::Error, } + if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)]) = (self.key, self.seed) { + let bytes = |w: [usize; 4]| -> [u8; 32] { core::array::from_fn(|i| w[i / 8].to_le_bytes()[i % 8]) }; + let (mut master, mut seed) = (bytes([k0[0], k0[1], k1[0], k1[1]]), bytes([s0[0], s0[1], s1[0], s1[1]])); + self.fs.unlock(&master, &seed); + master.fill(0); + seed.fill(0); + self.key = [None; 2]; + self.seed = [None; 2]; + self.unlocked = true; + } + StorageResponse::Ok } - /// Whether the whole master key has arrived. + /// Whether the encrypted areas are open: the key and seed are in. pub fn unlocked(&self) -> bool { - self.key.iter().all(Option::is_some) + self.unlocked } pub fn filesystem(&self) -> &F { @@ -225,7 +238,7 @@ impl StorageServer { Write { handle, .. } => self .owner(handle) .is_some_and(|o| o.badge == badge && o.write), - Create { .. } | Delete { .. } | Mkdir { .. } => scope.write, + Create { .. } | Delete { .. } | Mkdir { .. } | Rmdir { .. } => scope.write, _ => true, }; if !allowed { @@ -233,7 +246,7 @@ impl StorageServer { } let mutates = matches!( request, - Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. } + Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. } | Rmdir { .. } ); if mutates && !self.fs.writable() { return (R::ReadOnly, None); @@ -263,6 +276,7 @@ impl StorageServer { | Create { path_len } | Delete { path_len } | Mkdir { path_len } + | Rmdir { path_len } | ListDir { path_len } => Some(path_len), _ => None, }; @@ -286,7 +300,7 @@ impl StorageServer { None => R::NotFound, }; let response = match request { - Attach | SetLog | SetKey { .. } | Release => R::Error, + Attach | SetLog | SetKey { .. } | SetSeed { .. } | Release => R::Error, Grant { path_len, write } => { let narrower = scope.narrow(&buf[sent(path_len)], write); return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) { @@ -353,6 +367,13 @@ impl StorageServer { R::Error } } + Rmdir { .. } => { + if fs.rmdir(path) { + R::Ok + } else { + R::Error + } + } }; (response, None) } @@ -389,7 +410,8 @@ impl Server for StorageServer { ) -> ([usize; 4], Option) { match StorageRequest::decode(words) { Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None), - Some(StorageRequest::SetKey { half, words }) => (self.set_key(badge, half, words).encode(), None), + Some(StorageRequest::SetKey { half, words }) => (self.set_half(badge, false, half, words).encode(), None), + Some(StorageRequest::SetSeed { half, words }) => (self.set_half(badge, true, half, words).encode(), None), // A badge storage never issued, or one it has taken back. Some(request) => match self.grants.scope(badge) { None => (StorageResponse::Denied.encode(), None), diff --git a/crates/servers/storage-server/src/tests/protocol.rs b/crates/servers/storage-server/src/tests/protocol.rs index 3c08618..b669a20 100644 --- a/crates/servers/storage-server/src/tests/protocol.rs +++ b/crates/servers/storage-server/src/tests/protocol.rs @@ -26,6 +26,8 @@ fn every_request_roundtrips() { }, StorageRequest::Release, StorageRequest::SetKey { half: 1, words: [7, 9] }, + StorageRequest::SetSeed { half: 0, words: [1, 2] }, + StorageRequest::Rmdir { path_len: 3 }, ] { assert_eq!(StorageRequest::decode(req.encode()), Some(req)); } diff --git a/crates/servers/storage-server/src/tests/server.rs b/crates/servers/storage-server/src/tests/server.rs index ea436c5..ef227b6 100644 --- a/crates/servers/storage-server/src/tests/server.rs +++ b/crates/servers/storage-server/src/tests/server.rs @@ -474,14 +474,19 @@ fn the_unbadged_holder_cannot_be_released() { } #[test] -fn only_init_sets_the_key_and_only_once() { +fn only_init_unlocks_and_only_once() { let mut s = server(); - let set = |s: &mut Srv, badge, half| call(s, badge, StorageRequest::SetKey { half, words: [1, 2] }).0; - assert_eq!(set(&mut s, 5, 0), StorageResponse::Error, "a client cannot"); - assert_eq!(set(&mut s, 0, 0), StorageResponse::Ok); - assert!(!s.unlocked()); - assert_eq!(set(&mut s, 0, 0), StorageResponse::Error, "nor can it be replaced"); - assert_eq!(set(&mut s, 0, 2), StorageResponse::Error, "two halves only"); - assert_eq!(set(&mut s, 0, 1), StorageResponse::Ok); + let set = |s: &mut Srv, badge, req| call(s, badge, req).0; + let key = |half| StorageRequest::SetKey { half, words: [1, 2] }; + let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] }; + assert_eq!(set(&mut s, 5, key(0)), StorageResponse::Error, "a client cannot"); + assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Ok); + assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced"); + assert_eq!(set(&mut s, 0, key(2)), StorageResponse::Error, "two halves only"); + assert_eq!(set(&mut s, 0, key(1)), StorageResponse::Ok); + assert_eq!(set(&mut s, 0, seed(0)), StorageResponse::Ok); + assert!(!s.unlocked(), "not without the whole seed"); + assert_eq!(set(&mut s, 0, seed(1)), StorageResponse::Ok); assert!(s.unlocked()); + assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again"); } diff --git a/crates/tools/app-image/src/main.rs b/crates/tools/app-image/src/main.rs index 283c5ae..ea4ecd9 100644 --- a/crates/tools/app-image/src/main.rs +++ b/crates/tools/app-image/src/main.rs @@ -2,8 +2,9 @@ //! the same strict parser the permission manager uses: //! //! app-image install -//! an installed, pre-approved app: /apps//, its decision granting -//! everything it asks for, and /data// if that includes storage +//! an installed, pre-approved app: /apps//, and (under factory/, +//! which init seeds the encrypted areas from) its decision granting +//! everything it asks for, and data// if that includes storage //! app-image stage //! an app folder the owner can install later (`install `) @@ -36,11 +37,11 @@ fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> { }; let mut out = [0u8; MAX_DECISION_TEXT]; let n = decision.write(&mut out); - let decisions = root.join("system/permissions"); + let decisions = root.join("factory/system/permissions"); fs::create_dir_all(&decisions).map_err(|e| e.to_string())?; fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?; if decision.granted.contains(Capability::Storage) { - fs::create_dir_all(root.join("data").join(m.id.as_str())).map_err(|e| e.to_string())?; + fs::create_dir_all(root.join("factory/data").join(m.id.as_str())).map_err(|e| e.to_string())?; } Ok(()) } diff --git a/crates/types/src/gatekeeper.rs b/crates/types/src/gatekeeper.rs index 7430140..425410c 100644 --- a/crates/types/src/gatekeeper.rs +++ b/crates/types/src/gatekeeper.rs @@ -1,11 +1,15 @@ //! Init and the gatekeeper: after handing it the keystore and randomness //! (device setup calls), init calls `UNLOCK`, which returns once the owner -//! is authenticated (`OK`) or the gatekeeper cannot go on (`FAILED`); then -//! `KEY`, whose reply is the 32-byte master key in its four words. The -//! gatekeeper then exits. Only the unbadged holder (init) is answered. +//! is authenticated (`OK`, word 1 non-zero if the master key is new: a +//! fresh device or one just wiped) or the gatekeeper cannot go on +//! (`FAILED`); then `KEY`, whose reply is the 32-byte master key in its four +//! words, and `SEED`, 32 fresh random bytes from the hardware for storage's +//! randomness this boot. The gatekeeper then exits. Only the unbadged +//! holder (init) is answered. pub const UNLOCK: usize = 1; pub const KEY: usize = 2; +pub const SEED: usize = 3; pub mod reply { pub const OK: usize = 0; diff --git a/crates/types/src/storage.rs b/crates/types/src/storage.rs index 6875d15..7f661fe 100644 --- a/crates/types/src/storage.rs +++ b/crates/types/src/storage.rs @@ -36,6 +36,11 @@ pub mod op { /// Half the master key (word 1: which half, words 2..4: it), from the /// unbadged holder (init) only, each half once. pub const SET_KEY: usize = 13; + /// Half the seed for storage's randomness this boot, as `SET_KEY`. The + /// encrypted areas open once the whole key and seed are in. + pub const SET_SEED: usize = 14; + /// Removes the (empty) directory at the path in the buffer. + pub const RMDIR: usize = 15; } /// Reply tags (word 0). diff --git a/docs/apps.md b/docs/apps.md index 86bbdfd..6dce91e 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -31,11 +31,13 @@ declares `display`. Nothing gets anything "just in case". ## Disk layout ``` -/home/ the owner's files; the shell's `/` +/home/ the owner's files; the shell's `/` (encrypted) /apps// an installed app: manifest.toml and its binary -/data// the app's private folder (its `storage` capability) -/system/permissions/ the owner's decisions, one file per app id +/data// the app's private folder (its `storage`) (encrypted, its own key) +/system/permissions/ the owner's decisions, one file per app id (encrypted) /bin/ system servers and the shell (init starts these) +/factory/ plaintext starter content the encrypted areas are + seeded from with a new master key ``` Only the permission manager reaches `/apps`, `/data` and `/system`. The @@ -89,8 +91,9 @@ granted = ["display"] ``` Written by the permission manager when the owner decides; apps on the -system image ship with their decisions already recorded (pre-approved by -whoever built the image). Changing one later is the owner's call, through +system image ship with their decisions already recorded under +`factory/system/` (pre-approved by whoever built the image), and init +seeds them into the encrypted `system/` on a fresh setup. Changing one later is the owner's call, through the permission manager. ## Permission manager diff --git a/docs/ipc.md b/docs/ipc.md index 84da8d3..02f9ae5 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -97,8 +97,14 @@ gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore (a small disk of its own, told from the data disk by its size) and the virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in, then `KEY`, whose reply is the master key; init passes it to storage -(`SET_KEY`, from the unbadged holder only, each half once) and only then -starts the permission manager and the desktop. If the gatekeeper cannot +(`SET_KEY`, from the unbadged holder only, each half once), and a fresh +32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot. +Storage's encrypted areas (`home/`, `system/`, each app's `data//`; +crate `filecrypt`, layer `storage_server::encrypted`) open once both are +in. With a new key (a fresh device, or one just wiped: `UNLOCK`'s word 1) +init empties those areas and seeds them from the plaintext `factory/`; +an area that is missing is seeded too. Only then does init start the +permission manager and the desktop. If the gatekeeper cannot unlock, init stops there. The gatekeeper exits, and its capabilities go with it. diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index fcc9c59..d6aba99 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -33,11 +33,13 @@ app() { staged() { local crate="$1" folder="$2" if [ -f "${BUILD_DIR}/${crate}" ]; then - "${APP_IMAGE[@]}" stage "${ROOT}/home/${folder}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}" + "${APP_IMAGE[@]}" stage "${ROOT}/factory/home/${folder}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}" fi } -mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/data" "${ROOT}/system/permissions" "${ROOT}/home" +# home/, system/ and data/ are encrypted, so not made here: init seeds them +# from factory/ (plaintext) once the gatekeeper has the key. +mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/factory/home" "${ROOT}/factory/system/permissions" "${ROOT}/factory/data" # System servers and the shell: init starts these. copy display-qemu display copy input-qemu input @@ -53,8 +55,8 @@ app ls app sleep app snake staged counter inbox/counter -# The owner's files. -cp -r disk/home/. "${ROOT}/home/" +# The owner's first files. +cp -r disk/home/. "${ROOT}/factory/home/" # Only the test image: its presence makes init run the self-test. if [ -n "${SMPTEST:-}" ]; then copy smptest smptest diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index 9312868..de7dafb 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -50,8 +50,8 @@ echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok boot 9999 1234 [ ${status} -eq 0 ] && status=$? second_ok=0 -grep -aq "the owner is in" "${LOG}" && second_ok=1 -echo "test-qemu: second boot: wrong passcode refused, right one unlocks: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)" +grep -aq "the owner is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1 +echo "test-qemu: second boot: wrong passcode refused, right one unlocks, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)" rm -f "${KEYS}" "${MON}" grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" @@ -62,8 +62,12 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0 elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0 + elif debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then + echo "test-qemu: /home/readme.txt is plaintext on the disk"; disk_ok=0 + elif ! debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then + echo "test-qemu: /home/readme.txt is not a sealed file"; disk_ok=0 else - echo "test-qemu: e2fsck clean, file on disk" + echo "test-qemu: e2fsck clean, file on disk, home sealed" fi rm -f "${LOG}.fsck" fi