feat(users): per-user sealed folders users/<id>/{home,apps,data,permissions}; storage SET_USER/LOCK/PURGE; init sign-in/sign-out loop (purge fresh/removed/guest, seed from factory, session start/stop); shell logout; pm user+system app shelves; terminal SIGNED_OUT; per-program stack (gatekeeper 12p); 128K load scratch

This commit is contained in:
2026-10-28 01:08:50 +01:00
parent 29a25bd8c2
commit 91f7d28834
38 changed files with 1156 additions and 378 deletions

View File

@@ -0,0 +1,88 @@
//! Who is signed in, as far as storage cares: init says whose areas open,
//! hands over the key and seed, and at sign-out locks everything again.
//! Every one of these is init's alone (the unbadged holder).
use types::user::UserFolder;
use super::{Host, StorageServer};
use crate::filesystem::{FileHandle, FileSystem};
use crate::grants::MAX_GRANTS;
use crate::protocol::{MAX_PATH, StorageResponse};
impl<F: FileSystem, H: Host> StorageServer<F, H> {
/// Once per sign-in, before the key.
pub(super) fn set_user(&mut self, badge: usize, id: u64, guest: bool) -> StorageResponse {
if badge != 0 || self.unlocked || self.user.is_some() {
return StorageResponse::Error;
}
self.user = Some(if guest { UserFolder::GUEST } else { UserFolder::of(id) });
StorageResponse::Ok
}
/// Each half only once per sign-in: nobody can replace the key or the
/// seed while someone is in. Without a user said first, refused.
pub(super) 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, self.user, halves.get_mut(half)) {
(0, false, Some(_), Some(slot @ None)) => *slot = Some(words),
_ => return StorageResponse::Error,
}
if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)], Some(user)) = (self.key, self.seed, self.user) {
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, user.as_bytes());
master.fill(0);
seed.fill(0);
self.key = [None; 2];
self.seed = [None; 2];
self.unlocked = true;
}
StorageResponse::Ok
}
/// Sign-out: every grant goes (and with them every app's files and
/// buffers), init's own files close, the key is forgotten.
pub(super) fn lock(&mut self, badge: usize) -> StorageResponse {
if badge != 0 {
return StorageResponse::Error;
}
let mut roots = [0usize; MAX_GRANTS];
let n = self.grants.roots(&mut roots);
for &root in &roots[..n] {
self.release(root);
}
for (raw, owner) in self.owners.iter_mut().enumerate() {
if owner.take().is_some() {
self.fs.close(FileHandle::new(raw as u16));
}
}
self.fs.lock();
self.key = [None; 2];
self.seed = [None; 2];
self.user = None;
self.unlocked = false;
StorageResponse::Ok
}
/// While nobody is in, so no one's open file can be pulled away.
pub(super) fn purge(&mut self, badge: usize, path_len: usize) -> StorageResponse {
if badge != 0 || self.unlocked || !self.fs.writable() {
return StorageResponse::Error;
}
let Some(client) = self.clients.iter().flatten().find(|c| c.badge == Some(0)) else {
return StorageResponse::NotAttached;
};
let mut path = [0u8; MAX_PATH];
let len = path_len.min(MAX_PATH).min(client.buf.len());
path[..len].copy_from_slice(&client.buf[..len]);
let path = &path[..len];
let path = path.strip_prefix(b"/").unwrap_or(path);
// `users` (everyone, on a new keystore) or what is in it.
let inside = path == b"users"
|| path.strip_prefix(b"users/").is_some_and(|rest| !rest.split(|b| *b == b'/').any(|p| p.is_empty() || p == b"." || p == b".."));
if !inside {
return StorageResponse::Denied;
}
if self.fs.purge(path) { StorageResponse::Ok } else { StorageResponse::Error }
}
}