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:
@@ -1,9 +1,12 @@
|
||||
//! The encrypted areas (see the vision's Encryption note), over any file
|
||||
//! system: each app's `data/<id>/`, the owner's `home/`, and `system/` are
|
||||
//! kept as `filecrypt` files under their own keys, derived from the
|
||||
//! master key; the rest (programs, `factory/`) is plaintext. Before the key
|
||||
//! arrives the encrypted areas refuse everything, and a plaintext file in
|
||||
//! one is refused too: nothing there is trusted unless it was sealed.
|
||||
//! The encrypted areas (see the vision's Encryption and Users notes), over
|
||||
//! any file system. Each user's folder `users/<id>/` holds their areas:
|
||||
//! `home/`, `permissions/`, and per app `apps/<app>/` and `data/<app>/`,
|
||||
//! each kept as `filecrypt` files under its own key, derived from that
|
||||
//! user's master key. Only the signed-in user's areas open; anyone else's
|
||||
//! are refused (and noise to their key anyway). The rest (programs,
|
||||
//! `factory/`) is plaintext. Signed out, every area refuses everything, and
|
||||
//! a plaintext file in one is refused too: nothing there is trusted unless
|
||||
//! it was sealed.
|
||||
|
||||
use filecrypt::{CHUNK, Drbg, HEADER, Header, Key, OVERHEAD, PLAIN_CHUNK, area_key, chunk_offset, chunks, plain_len};
|
||||
|
||||
@@ -22,15 +25,28 @@ enum Area<'a> {
|
||||
Neither,
|
||||
}
|
||||
|
||||
fn area(path: &[u8]) -> Area<'_> {
|
||||
/// Where `path` falls, and whose it is: the user folder's name.
|
||||
fn area(path: &[u8]) -> (Area<'_>, Option<&[u8]>) {
|
||||
let path = path.strip_prefix(b"/").unwrap_or(path);
|
||||
let mut parts = path.splitn(3, |b| *b == b'/');
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(b"home" | b"system"), _, _) => Area::Sealed(&path[..path.iter().position(|b| *b == b'/').unwrap_or(path.len())]),
|
||||
(Some(b"data"), Some(id), Some(rest)) if !id.is_empty() && !rest.is_empty() => Area::Sealed(&path[..5 + id.len()]),
|
||||
(Some(b"data"), _, _) => Area::Neither,
|
||||
_ => Area::Plain,
|
||||
let mut parts = path.splitn(5, |b| *b == b'/');
|
||||
let (users, user, kind, app, rest) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next());
|
||||
if users != Some(b"users") {
|
||||
return (Area::Plain, None);
|
||||
}
|
||||
// `users/` itself holds only the folders, which anyone may list.
|
||||
let Some(user) = user else { return (Area::Plain, None) };
|
||||
if user.is_empty() {
|
||||
return (Area::Neither, None);
|
||||
}
|
||||
// `users/<id>/` up to and including `kind`, and the app's folder.
|
||||
let upto = |n: usize| &path[..n];
|
||||
let base = 6 + user.len() + 1;
|
||||
let area = match (kind, app, rest) {
|
||||
(Some(k @ (b"home" | b"permissions")), _, _) => Area::Sealed(upto(base + k.len())),
|
||||
(Some(k @ (b"apps" | b"data")), Some(a), Some(r)) if !a.is_empty() && !r.is_empty() => Area::Sealed(upto(base + k.len() + 1 + a.len())),
|
||||
_ => Area::Neither,
|
||||
};
|
||||
(area, Some(user))
|
||||
}
|
||||
|
||||
/// An open encrypted file.
|
||||
@@ -44,15 +60,20 @@ struct Open {
|
||||
|
||||
pub struct Encrypted<F: FileSystem> {
|
||||
inner: F,
|
||||
/// The master key and randomness, once the gatekeeper has unlocked.
|
||||
/// The signed-in user's master key and folder name, and randomness.
|
||||
master: Option<Key>,
|
||||
user: [u8; USER_NAME],
|
||||
user_len: usize,
|
||||
rng: Option<Drbg>,
|
||||
open: [Option<Open>; MAX_HANDLES],
|
||||
}
|
||||
|
||||
/// A user folder's name: 16 hex digits, or `guest`.
|
||||
pub const USER_NAME: usize = 16;
|
||||
|
||||
impl<F: FileSystem> Encrypted<F> {
|
||||
pub fn new(inner: F) -> Self {
|
||||
Self { inner, master: None, rng: None, open: [const { None }; MAX_HANDLES] }
|
||||
Self { inner, master: None, user: [0; USER_NAME], user_len: 0, rng: None, open: [const { None }; MAX_HANDLES] }
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &F {
|
||||
@@ -68,21 +89,27 @@ impl<F: FileSystem> Encrypted<F> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// The key of `path`'s area, if it is encrypted and the store unlocked:
|
||||
/// Whether `user`'s folder is the signed-in user's.
|
||||
fn signed_in(&self, user: Option<&[u8]>) -> bool {
|
||||
self.master.is_some() && user == Some(&self.user[..self.user_len])
|
||||
}
|
||||
|
||||
/// The key of `path`'s area, if it is encrypted and its user signed in:
|
||||
/// `Ok(None)` for a plaintext path, `Err` for one it must refuse.
|
||||
fn key_for(&self, path: &[u8]) -> Result<Option<Key>, ()> {
|
||||
match area(path) {
|
||||
Area::Plain => Ok(None),
|
||||
Area::Neither => Err(()),
|
||||
Area::Sealed(name) => self.master.as_ref().map(|m| Some(area_key(m, name))).ok_or(()),
|
||||
(Area::Plain, _) => Ok(None),
|
||||
(Area::Sealed(name), user) if self.signed_in(user) => Ok(self.master.as_ref().map(|m| area_key(m, name))),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `path` may be used as a directory (made, listed, removed).
|
||||
/// Whether `path` may be used as a directory (made, listed, removed):
|
||||
/// anywhere plaintext, and in the signed-in user's own folder.
|
||||
fn dir_allowed(&self, path: &[u8]) -> bool {
|
||||
match area(path) {
|
||||
Area::Plain => true,
|
||||
_ => self.master.is_some(),
|
||||
(Area::Plain, _) => true,
|
||||
(_, user) => self.signed_in(user),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,13 +142,33 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
self.inner.writable()
|
||||
}
|
||||
|
||||
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32]) {
|
||||
if self.master.is_none() {
|
||||
fn unlock(&mut self, master: &[u8; 32], seed: &[u8; 32], user: &[u8]) {
|
||||
if self.master.is_none() && !user.is_empty() && user.len() <= USER_NAME {
|
||||
self.master = Some(Key(*master));
|
||||
self.rng = Some(Drbg::new(*seed));
|
||||
self.user[..user.len()].copy_from_slice(user);
|
||||
self.user_len = user.len();
|
||||
}
|
||||
}
|
||||
|
||||
fn lock(&mut self) {
|
||||
for (i, slot) in self.open.iter_mut().enumerate() {
|
||||
if slot.take().is_some() {
|
||||
self.inner.close(FileHandle::new(i as u16));
|
||||
}
|
||||
}
|
||||
self.master = None;
|
||||
self.rng = None;
|
||||
self.user = [0; USER_NAME];
|
||||
self.user_len = 0;
|
||||
}
|
||||
|
||||
/// Removes a whole tree without opening anything in it: a removed
|
||||
/// user's or a guest's files go though no key for them is held.
|
||||
fn purge(&mut self, path: &[u8]) -> bool {
|
||||
crate::filesystem::remove_tree(&mut self.inner, path)
|
||||
}
|
||||
|
||||
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
let key = self.key_for(path).ok()?;
|
||||
let h = self.inner.open(path)?;
|
||||
@@ -242,8 +289,8 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
}
|
||||
let n = self.inner.list_dir(path, entries)?;
|
||||
// In an encrypted area a file's size is what it holds, not its disk size.
|
||||
// (Listing an app's own folder, `data/<id>`, shows its sealed files.)
|
||||
let sealed = area(path) != Area::Plain;
|
||||
// In a user's folder, a file's size is what it holds.
|
||||
let sealed = area(path).0 != Area::Plain;
|
||||
for e in entries[..n].iter_mut().filter(|e| sealed && !e.is_dir) {
|
||||
e.size = plain_len(e.size).unwrap_or(0);
|
||||
}
|
||||
@@ -265,7 +312,7 @@ impl<F: FileSystem> FileSystem for Encrypted<F> {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn area_of(path: &[u8]) -> &'static str {
|
||||
match area(path) {
|
||||
match area(path).0 {
|
||||
Area::Plain => "plain",
|
||||
Area::Sealed(_) => "sealed",
|
||||
Area::Neither => "neither",
|
||||
|
||||
Reference in New Issue
Block a user