secrets landed: archive TASK-11, storage-encryption status, handoff
souveraine-secrets is live on the phone (reboot-verified, keyring masked) — TASK-11 complete, moved to tasks/archive/ with the outcome. STORAGE-ENCRYPTION.md: add the doc (design of record, 2026-07-20) with status updated to match code — secrets-store AEAD + machine wrap built, Argon2id wrap built with ingress pending; the derive_storage_key gap paragraph now scoped to what remains. Add session handoff.
This commit is contained in:
parent
b736139590
commit
ae6e2be533
3 changed files with 461 additions and 0 deletions
304
docs/STORAGE-ENCRYPTION.md
Normal file
304
docs/STORAGE-ENCRYPTION.md
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
---
|
||||
description: Storage encryption at rest — three-class model, per-agent keys, hardware-throttled unlock, portal-shaped permissions
|
||||
status: Design of record — secrets store built (AEAD, machine wrap, Argon2id wrap slot; live on the phone); rest of userspace greenfield, kernel primitives present
|
||||
date: 2026-07-20
|
||||
---
|
||||
|
||||
# Storage encryption — design of record
|
||||
|
||||
User data on a SouveraineOS device must be **cryptographically inert when the
|
||||
device is locked**, not merely permission-hidden. A stolen powered-off phone
|
||||
should yield ciphertext and nothing else. This is the property Android calls
|
||||
File-Based Encryption and iOS calls Data Protection; we have neither today.
|
||||
|
||||
This is the end-state design. It records what the platform can already do
|
||||
(with citations), what must be built, and where a decision is deliberately
|
||||
deferred. Where this and the code disagree, the code is right — fix one or the
|
||||
other in the same change. No v1/v2: the shape below is the whole product;
|
||||
components light up as their backends earn them (doctrine §6).
|
||||
|
||||
## The problem, measured
|
||||
|
||||
On the reference laptop, `~/.souveraine` today:
|
||||
|
||||
```
|
||||
agents/ 69M world-readable, default perms
|
||||
subconscious-agents/ 30M world-readable
|
||||
server/ 24M world-readable
|
||||
events/ 7.6M world-readable
|
||||
secrets/store.json 0600, AES-256-CBC, seed-derived key
|
||||
seed-id/private.key 32B 0600
|
||||
```
|
||||
|
||||
The one encrypted file is a rounding error beside ~130M of plaintext
|
||||
conversation history, memory, and ledger. When this was first measured
|
||||
(2026-07-20 morning), the store's key derived entirely from a seed file
|
||||
sitting on the same disk and the tree had no Argon2/AEAD at all — 0600 as
|
||||
the only real barrier, no secret living only in the user's head. That gap
|
||||
is now closed **for the secrets store**: items are sealed AES-256-GCM under
|
||||
a random store key, wrapped by a machined signature (private key outside
|
||||
the daemon) and optionally by an Argon2id passphrase slot
|
||||
(`src/secrets/storage_key.rs`). The measurement stands for everything
|
||||
else in `~/.souveraine` — the ~130M of plaintext is still the problem
|
||||
this design exists to solve.
|
||||
|
||||
## Three classes
|
||||
|
||||
Modelled on iOS Data Protection classes and Android DE/CE, but with a
|
||||
deliberate difference: **most data evicts its key on lock.** No shipping OS
|
||||
does this — Android never evicts on lock (only at reboot), and iOS's
|
||||
evict-on-lock class (A) is the exception, not the default. This is a novel
|
||||
synthesis and its costs are named honestly below.
|
||||
|
||||
| Class | Key lifetime | Holds | Analogue |
|
||||
| --- | --- | --- | --- |
|
||||
| **Ambient** | Resident from boot; machine-seed-rooted, no human present | Clock, weather, timers, alarms, transport controls, modem, boot path | Android DE / iOS Class C-ish |
|
||||
| **Personal** | Installed on first unlock; **evicted on lock** | Photos, messages, payment credentials, agent memory, calendar, conversation history | iOS Class A (`NSFileProtectionComplete`) |
|
||||
| **Revealable** | Resident while locked, so the lockscreen can display it | Notification previews, now-playing metadata, next-alarm text | iOS Class C, scoped tiny |
|
||||
|
||||
Maps onto the existing capability tiers (`SESSION-TRUST-ARCHITECTURE.md`):
|
||||
Ambient → ambient tier, Personal → personal/stepUp, Revealable is the storage
|
||||
face of doctrine §2's **reveal-on-lock-surface** feature — a fingerprint touch
|
||||
reveals personal content *in place* without unlocking. Revealable exists
|
||||
precisely so that feature survives key eviction: it is the only Personal-class
|
||||
data whose key we deliberately keep live while locked.
|
||||
|
||||
**Scope Revealable ruthlessly.** Everything kept resident while locked is, by
|
||||
construction, After-First-Unlock attack surface. Notification preview text,
|
||||
media metadata, alarm strings. Not message bodies, not thumbnails, not memory.
|
||||
When in doubt, it is Personal.
|
||||
|
||||
## Key hierarchy
|
||||
|
||||
The doctrine already separates machine identity from agent identity
|
||||
(`saf/identity/01-seed-identity.md`, `summon.rs:1-14`): the machine seed signs
|
||||
"this left this box," the agent seed signs "this was done by her." That split
|
||||
is the encryption hierarchy.
|
||||
|
||||
```
|
||||
Machine seed (Ed25519, /var/lib/souveraine/seed-id, machined)
|
||||
└─ HKDF → Ambient class key [headless, boot-available]
|
||||
|
||||
User PIN/passphrase
|
||||
└─ Argon2id(salt) → KEK
|
||||
└─ (target) sealed by hardware throttle — see "Unlock"
|
||||
└─ wraps → Personal class key [evicted on lock]
|
||||
└─ wraps → Revealable class key [stays resident while locked]
|
||||
|
||||
Per-agent identity (own UNIX user — see below)
|
||||
└─ agent's own PIN/credential → Argon2id → agent KEK
|
||||
└─ wraps → that agent's Personal class key
|
||||
```
|
||||
|
||||
The KEK wraps the class keys; the class keys are the fscrypt master keys. A
|
||||
password change re-wraps the class keys — it never re-encrypts data and never
|
||||
orphans it. Recovery (forgotten PIN) is a recovery-key wrap of the same class
|
||||
keys, generated at provision time; without it, forgotten PIN = data gone, by
|
||||
design.
|
||||
|
||||
### Annie is her own user
|
||||
|
||||
Agents get their own UNIX accounts with scoped permissions. This is the P3 fix
|
||||
in `audit-status.md` — per-agent accounts are named there as the enabler for
|
||||
caller identity (P0/P1/P2) — and it resolves the "can Aster think while locked"
|
||||
problem for free: her memory is Personal-class **under her uid, keyed by her
|
||||
credential**, not the user's. The user locking their session does not evict
|
||||
her key. She keeps her own memory, cron-summons herself, reconciles at her own
|
||||
pace. Android's per-app-UID isolation, applied to agents.
|
||||
|
||||
This also means agent memory encryption and the per-agent-account work are one
|
||||
project, not two.
|
||||
|
||||
## Unlock — binding the PIN to hardware
|
||||
|
||||
The user unlocks the phone by swiping up and typing a PIN. A PIN is
|
||||
low-entropy; a million guesses fall in under a second to any GPU against any
|
||||
KDF. Android survives this because **Titan M** holds the secret and
|
||||
rate-limits attempts in hardware (Weaver + Gatekeeper): ~20 tries, exponential
|
||||
backoff, unbypassable even under full TEE compromise because it is a separate
|
||||
secure element.
|
||||
|
||||
**State of the hardware path on blueline (verified):**
|
||||
|
||||
- Titan M is present. Its interface is `citadel-spi` — a small SPI transport
|
||||
driver (`/dev/citadelN`, two ioctls) that exists **only in the LOS 4.9
|
||||
reference tree**, not in our mainline kernel. The Weaver wire protocol is
|
||||
public (AOSP `platform/external/nos/host/generic`, Quarkslab's `titanm`
|
||||
toolkit).
|
||||
- `CONFIG_QCOMTEE=m` gives a real mainline Qualcomm TEE client, but
|
||||
`CONFIG_TRUSTED_KEYS` is **off**, and even flipped on there is no known
|
||||
loadable QSEE trustlet doing HUK-sealing for it to talk to. Plumbing without
|
||||
a cryptographic endpoint.
|
||||
- postmarketOS/Mobian on Qualcomm do plain passphrase-derived LUKS, **no
|
||||
hardware throttling anywhere**. There is no prior art to lean on; the
|
||||
reference is the LineageOS slot on this same device.
|
||||
|
||||
**Consequence — two milestones, not two products:**
|
||||
|
||||
1. **Ship:** Argon2id over a real passphrase (not a 6-digit PIN) at first
|
||||
unlock. Aggressive parameters, honest about the ceiling: no hardware root of
|
||||
trust, so a passphrase with genuine entropy is required for the at-rest
|
||||
claim to mean anything. A 6-digit PIN here is theatre.
|
||||
2. **Target:** port `citadel-spi` (bounded — a few hundred lines, self-contained
|
||||
SPI/GPIO, model the DT node on `sdm845-b1c1-citadel.dtsi`) + a minimal Linux
|
||||
`libnos_datagram` backend speaking Weaver only. Then the PIN binds to
|
||||
hardware throttling and the "swipe-up-and-type-a-PIN" UX becomes secure.
|
||||
Inherits Titan M's known (Quarkslab-documented) vulnerability surface —
|
||||
using it means trusting a chip that has been partially broken.
|
||||
|
||||
The design assumes milestone 2 as the target and degrades honestly to
|
||||
milestone 1 until the citadel port lands. Kernel rebuilds to enable
|
||||
`TRUSTED_KEYS`/`ENCRYPTED_KEYS`/`PERSISTENT_KEYRINGS`/`ANDROID_BINDERFS` are in
|
||||
scope — that is what we do.
|
||||
|
||||
## fscrypt is the mechanism
|
||||
|
||||
Not dm-crypt. fscrypt is per-directory with independent keys, which is what
|
||||
lets three classes and multiple per-agent keys coexist on one filesystem;
|
||||
whole-disk dm-crypt gives one key for everything and cannot express the split.
|
||||
|
||||
**Platform (verified on blueline):** `CONFIG_FS_ENCRYPTION=y`,
|
||||
`FS_ENCRYPTION_INLINE_CRYPT=y`, `BLK_INLINE_ENCRYPTION=y`, and the Snapdragon
|
||||
845 Inline Crypto Engine is fully wired — `CONFIG_QCOM_INLINE_CRYPTO_ENGINE=y`,
|
||||
driver `drivers/soc/qcom/ice.c` feeding a `blk_crypto_profile` via
|
||||
`ufs-qcom.c`. Encryption runs in the storage controller at near-zero CPU cost,
|
||||
the same engine Android uses for FBE here. Root is ext4, which supports fscrypt
|
||||
natively.
|
||||
|
||||
**Provisioning (Pixel3Arch):** the rootfs is baked by `mkfs.ext4 -d` populating
|
||||
a tree (`provision-rootfs.sh:220`). fscrypt needs `-O encrypt` on the fs and a
|
||||
policy set on the target dirs *before* population — a change at that line plus a
|
||||
login-time unlock hook in `rootfs-overlay/etc/pam.d/`. The `fscrypt` userspace
|
||||
tool is not yet packaged; it must be. A custom initramfs hook (alongside
|
||||
`blueline-debug`) is the place for any pre-`switch_root` key supply. Migration
|
||||
of existing data is a non-issue — pre-users, provision fresh.
|
||||
|
||||
**Known fscrypt limits (from kernel docs, carry into implementation):**
|
||||
|
||||
- Key removal while mounted works but does **not** wipe per-file keys for
|
||||
already-open files (`FILES_BUSY`, retryable). A process holding a Personal
|
||||
file open at lock keeps reading it until it closes. Eviction must be paired
|
||||
with closing handles.
|
||||
- Dirty-page / writeback behaviour on key removal is **undocumented** in the
|
||||
kernel itself. Test it; do not assume.
|
||||
- Evicting a key does nothing about plaintext already in page cache, app
|
||||
memory, or swap. This is the single most-ignored gap across Android, iOS, and
|
||||
fscrypt alike. Handle separately: disable/encrypt swap, and treat the
|
||||
eviction as "no new reads," not "RAM is clean."
|
||||
|
||||
## Eviction on lock
|
||||
|
||||
Wire to the authoritative bit. `screenLockSecure` is set in exactly one place
|
||||
(`LockScreen.qml:124`, from `WlSessionLock.onSecureChanged`) and is the only
|
||||
proof suitable for personal disclosure; `screenLocked` is just the request.
|
||||
Evict on `screenLockSecure`, never on `screenLocked`.
|
||||
|
||||
The precedent exists: `StepUpAuth.qml:345` already does `revokeAll()` of auth
|
||||
grants on lock, fail-closed. Key eviction is structurally identical — same
|
||||
`Connections { target: GlobalStates }` hook, different resource. `Ai.qml:181`
|
||||
already redacts in-flight output on lock; `SessionAudit.qml:145` hash-chains an
|
||||
entry on every transition and a key-eviction event should feed it too.
|
||||
|
||||
**The gap:** `souveraine-secrets` has zero lock awareness — it derives its key
|
||||
once at startup and holds it for process life; `Unlock`/`Lock` are no-ops
|
||||
(`service.rs`, `collection.rs:139`). And **no D-Bus signal is emitted on
|
||||
lock/unlock today** — SessionEvents only consumes logind signals. Options for
|
||||
delivery, decision deferred:
|
||||
|
||||
1. The secrets daemon grows its own logind ingress (`gdbus monitor`, same shape
|
||||
as `SessionEvents.qml`; doctrine §6 permits).
|
||||
2. It becomes a client of `sessiond`'s socket and drives eviction off `Phase`.
|
||||
3. `sessiond` gains a notification verb.
|
||||
|
||||
`Unlock` stops being a no-op once the daemon consults lock state; this closes
|
||||
`audit-status.md` P2 as a consequence, not separate work.
|
||||
|
||||
## Federation — encrypt then push
|
||||
|
||||
The Gitea remote is **untrusted by design.** It is trusted in the reference
|
||||
deployment's private network, but the product cannot assume everyone has one —
|
||||
if confidentiality depended on the network being private, the software would
|
||||
only work in the author's house. So ciphertext crosses; TLS/PQC on the wire is
|
||||
hardening on top, not the confidentiality boundary.
|
||||
|
||||
Memory sync landed (`a62379a`, push/fetch to `instance/{label}` branches),
|
||||
superseding FEDERATION.md gap 1 — fix that doc in the same change.
|
||||
|
||||
**Consequence for merge:** encrypted content breaks git's three-way merge —
|
||||
git cannot diff what it cannot read, so FEDERATION.md's domain-aware merge
|
||||
policy (`journal/` auto-merge, `system/` never, else three-way) must run on
|
||||
**plaintext, locally, after fetch+decrypt**, then re-encrypt on push. The DAG
|
||||
still models divergence; reconciliation moves into our code. A `system/`
|
||||
conflict is a key-split event, surfaced as felt, not auto-unioned.
|
||||
|
||||
**Per-node key wrapping, not shared keys.** Doctrine forbids the agent root key
|
||||
travelling to a node (`saf/identity/01-seed-identity.md`: "The root key does not
|
||||
travel to a federated node"; a copied key "cannot distinguish a legitimate fork
|
||||
from a stolen duplicate"). So Personal-class content keys are wrapped **per
|
||||
commissioned node** — each device gets its own wrapping via the commissioning
|
||||
ceremony (`node.rs`, currently `#![allow(dead_code)]`). Decommission a node →
|
||||
stop re-wrapping for it → revocation with teeth. This is the Signal/Matrix
|
||||
multi-device model, and it is what makes encrypt-then-push to untrusted storage
|
||||
sound.
|
||||
|
||||
**Sequencing:** per-node wrapping cannot ship before commissioning is real
|
||||
(`provisioning-gaps.md`: first-boot commissioning is manual today). Encryption
|
||||
and the commissioning ceremony are one project. The legacy pre-split seed
|
||||
(`seed-id.pre-split-20260716` on the phone) is an artifact of the superseded
|
||||
one-seed model; the phone should get a *node* key via commissioning, not a
|
||||
restored old seed. Restore-vs-regenerate is therefore mostly moot — the only
|
||||
live question is whether anything under the old seed needs recovering, and
|
||||
`store.json` is the sole seed-derived ciphertext (pre-users: nothing).
|
||||
|
||||
## App permission gating (separate track, shared foundation)
|
||||
|
||||
Encryption-at-rest and permission-gating are the same project from two angles;
|
||||
both rest on **identity per application**. Recorded here so the foundation is
|
||||
built once; the gating design is its own doc.
|
||||
|
||||
- **Surface:** `org.freedesktop.portal.*` (Documents, Camera, Location, Secret)
|
||||
— the interface third-party software already speaks. Not a bespoke Souveraine
|
||||
API. Doctrine §6: only expose a kind once it owns a real backend. Design the
|
||||
full contract once; light up backends as they earn it (this is the anti-v1/v2
|
||||
discipline, not a crippled first slice).
|
||||
- **Identity, three tiers:** Flatpak app ID from the bwrap sandbox (free, once
|
||||
Flatpak is installed — it is not today), Waydroid/Android UID inside the
|
||||
container (free once binder is up — `ANDROID_BINDER_IPC=y`, but
|
||||
`ANDROID_BINDERFS` off is a real Waydroid blocker, kernel change), per-agent
|
||||
UNIX accounts for native callers (the Personal-class work above).
|
||||
- **Nothing installed today:** no Flatpak, bubblewrap, xdg-desktop-portal, or
|
||||
Waydroid in either repo. The namespace/cgroup/seccomp primitives are compiled
|
||||
in and unused. "Free granularity from Flatpak" is a design premise that
|
||||
requires installing Flatpak first.
|
||||
- **Runtime vs at-rest:** fscrypt protects a powered-off device. It does not
|
||||
stop a process in the unlocked session reading another's files — that is the
|
||||
runtime half, and per-app UIDs are its foundation too. At-rest encryption
|
||||
alone must not be sold as "app data is locked down"; the runtime half is
|
||||
per-app identity, tracked separately.
|
||||
|
||||
## Also found while mapping (fix independently)
|
||||
|
||||
- **Gitea tokens in plaintext.** Two agents have `http://<token>@10.10.20.120:4455/...`
|
||||
remotes — credentials in `.git/config` at default perms next to a 0600 seed,
|
||||
and `http://` so tokens cross the LAN in clear on every push. Not git-tracked,
|
||||
nothing leaked upstream. Survivable on a private LAN, not as shipped product.
|
||||
- **Agent seeds safe positionally, not by design.** `agents/{id}/seed/` is a
|
||||
sibling of the `memory/` git root, so `sync` structurally cannot push private
|
||||
keys (verified: `git ls-files | grep seed` empty on live agents). But nothing
|
||||
enforces it — no `.gitignore`, no runtime check. Harden as part of this work.
|
||||
|
||||
## Status
|
||||
|
||||
| Piece | State |
|
||||
| --- | --- |
|
||||
| Kernel crypto primitives (ext4 fscrypt, ICE inline) | Present, verified, unused |
|
||||
| `TRUSTED_KEYS`/`ENCRYPTED_KEYS`/`PERSISTENT_KEYRINGS`/`BINDERFS` | Off — kernel rebuild in scope |
|
||||
| fscrypt userspace + provisioning hook | Not built; hook points identified |
|
||||
| Three-class model | Designed here; nothing built |
|
||||
| Secrets store AEAD (AES-256-GCM, machine wrap via machined) | **Built**, live on the phone, reboot-verified |
|
||||
| Argon2id passphrase unlock (milestone 1) | **Built** for the secrets store key wrap (`Manage` verbs live); ingress (stepUp/PAM) pending, options 1-3 undecided |
|
||||
| citadel-spi port + Weaver (milestone 2, hardware throttle) | Not built; bounded, referenced |
|
||||
| Per-agent UNIX accounts + own-key memory | Not built; enables P0-P3 |
|
||||
| Eviction-on-lock hook + secrets lock-awareness | Not built; precedent + delivery options identified |
|
||||
| Per-node key wrapping | Blocked on commissioning ceremony (dead-code today) |
|
||||
| Portal permission backends | Not built; nothing installed |
|
||||
140
docs/handoff-2026-07-20-secrets.md
Normal file
140
docs/handoff-2026-07-20-secrets.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Handoff — 2026-07-20: souveraine-secrets live on the phone
|
||||
|
||||
Written for the next session (Fable). Everything below marked VERIFIED was
|
||||
proven live this session, on-device or on archdev — not asserted.
|
||||
|
||||
## What shipped (VERIFIED)
|
||||
|
||||
**souveraine-secrets is the phone's Secret Service.** It owns
|
||||
`org.freedesktop.secrets` on the Pixel 3, from boot, surviving a full
|
||||
reboot; gnome-keyring is masked and not running; a `secret-tool`
|
||||
store/lookup round-trip passes, and a stored secret written before reboot
|
||||
decrypted after it. Daemon journal is clean; unit
|
||||
`souveraine-secrets.service` (user, Type=dbus, WantedBy=default.target) +
|
||||
D-Bus activation shadow at
|
||||
`/usr/local/share/dbus-1/services/org.freedesktop.secrets.service`.
|
||||
Deploy = `souveraine/packaging/deploy-secrets-phone.sh` (idempotent,
|
||||
pulls the archdev cross-build, masks keyring, verifies round-trip).
|
||||
|
||||
**Key hierarchy per STORAGE-ENCRYPTION.md** (the design of record, dated
|
||||
2026-07-20 — read it first):
|
||||
- random 32-byte store key, items sealed AES-256-GCM (item id = AAD);
|
||||
- `machine` wrap: HKDF of a deterministic machined signature
|
||||
(domain `secrets-store-key`, socket `/run/souveraine/machined.sock`;
|
||||
legacy `~/.souveraine/seed-id` fallback with identical framing — the
|
||||
machine private key never enters the daemon). VERIFIED on-device:
|
||||
`source=Machined`, key stable across reboot.
|
||||
- `passphrase` wrap slot: Argon2id (64 MiB/3 passes, params in header,
|
||||
salted, HKDF-mixed with the machine signature so off-device brute force
|
||||
needs the machine seed too). Enrolled/rotated/verified via
|
||||
`org.souveraine.Secrets.Manage` at `/org/souveraine/secrets`
|
||||
(SetPassphrase / VerifyPassphrase / HasPassphrase). Rotation re-wraps
|
||||
only, never re-encrypts items. NOT yet driven by anything — the stepUp
|
||||
dialog (PIN, later fingerprint) is the intended caller after PAM
|
||||
verifies; the lock-signal/eviction ingress choice (doc options 1/2/3)
|
||||
is deliberately still Casey's open decision.
|
||||
- Lock semantics: store unlocks with the *user session*; screen lock does
|
||||
not lock it (gnome-keyring's stance; background streaming/Matrix need
|
||||
it). Documented in the bin header.
|
||||
|
||||
**Spec fixes found by real clients (regression-tested):**
|
||||
- DH group was WRONG — the constant under `MODP_1024_PRIME_HEX` was the
|
||||
RFC 3526 2048-bit group-14 prime; libsecret uses the true 1024-bit
|
||||
Second Oakley group. Every libsecret session failed its first decrypt.
|
||||
Fixed + pinned by test (`dh.rs::prime_is_the_1024_bit_oakley_group`).
|
||||
- Item ids were hyphenated UUIDs — `-` is illegal in D-Bus object paths
|
||||
(CreateItem failed). Now `Uuid::simple()`.
|
||||
- Collection must also be served at `/org/freedesktop/secrets/aliases/default`
|
||||
(libsecret addresses the alias path directly, not via ReadAlias).
|
||||
- Interfaces now register via `Builder::serve_at` BEFORE the name is
|
||||
claimed (startup race).
|
||||
- Added: spec signals (ItemCreated/Changed/Deleted, Collection*), real
|
||||
created/modified timestamps, stored content-types, atomic 0600 store
|
||||
writes. 19 unit tests green on archdev.
|
||||
|
||||
**souveraine-player Navidrome login persists.** `src/secrets.rs`
|
||||
(secret-service crate 5, DH), ServerConnect{remember}/ServerRestore/
|
||||
ServerLogout commands, ServerBrowser `server`+`restoring` properties +
|
||||
`logout()`, remember-me checkbox, login form hidden until the startup
|
||||
restore resolves. Cross-built on archdev, deployed to the phone.
|
||||
COMMITTED (`souveraine-player` 313481d, branch master). The
|
||||
crate↔daemon DH path is VERIFIED (probe on archdev private bus:
|
||||
store/search/read/delete round-trip OK) — the QML flow itself is NOT yet
|
||||
tested on-device (Casey does app testing).
|
||||
|
||||
**culver** already had full Secret Service session persistence
|
||||
(`crates/matrix/src/login.rs` — store/load/restore + pointer file; app
|
||||
main.rs wires login_password/restore_session). Its CI failure was two
|
||||
clippy lints; fixed and pushed (culver d620113, branch primary — check
|
||||
the CI run result). No culver logout verb exists yet.
|
||||
|
||||
## Uncommitted / left for next session
|
||||
|
||||
1. **`~/Projects/souveraine` — the whole daemon arc is UNCOMMITTED**
|
||||
(M: Cargo.toml, src/bin/souveraine-secrets.rs, src/secrets/{collection,
|
||||
dh,item,service,store}.rs; new: src/secrets/{manage,storage_key}.rs,
|
||||
packaging/{deploy-secrets-phone.sh,org.freedesktop.secrets.service,
|
||||
souveraine-secrets.service}). Branch `experimental/session-trust-phase1`.
|
||||
NOTE: laptop Cargo.lock is STALE — deps (aes-gcm, argon2, tempfile)
|
||||
were resolved on archdev in `~/Projects/souveraine-fable`; copy the
|
||||
lock back or regenerate before committing.
|
||||
2. **SouveraineOS docs**: archive TASK-11 (done — create
|
||||
`docs/tasks/archive/`, move `11-souveraine-secrets.md` with a
|
||||
completion note), update STORAGE-ENCRYPTION.md status table (AEAD:
|
||||
built for the secrets store; Argon2id passphrase wrap: built, ingress
|
||||
pending; "the gap" paragraph about `derive_storage_key` is now stale —
|
||||
the doc says code wins, fix the doc). Casey has his OWN in-flight
|
||||
edits in that repo (LOCK-DPMS-LESSONS.md, tasks/README.md) — commit
|
||||
only what you touched.
|
||||
3. This handoff file itself.
|
||||
|
||||
## Next session's list (Casey's words)
|
||||
|
||||
- Commit everything committable (above).
|
||||
- **Audit the boot path a bit.**
|
||||
- **Add icons to these apps** (souveraine-player, culver .desktop entries).
|
||||
- **Test the apps on-device**: player login → remember → restart →
|
||||
auto-reconnect → logout; culver Matrix login persistence; ALSO confirm
|
||||
the lockscreen PIN still works post-keyring-removal (PAM has no
|
||||
keyring module — verified absent — but confirm by hand).
|
||||
- stepUp dialog work: PIN/fingerprint confirm popup; when it lands, wire
|
||||
its PAM success to `Manage.SetPassphrase` so the lockscreen passphrase
|
||||
enrolls the at-rest wrap, and `VerifyPassphrase` to detect drift after
|
||||
password changes.
|
||||
|
||||
## Phone-as-node (Casey asked)
|
||||
|
||||
Handoff/design exists and is sufficient to run from a laptop session:
|
||||
`souveraine/saf/federation/01-node-enrollment.md` (ceremony + command
|
||||
shapes + implementation order; step 1 — versioned structs + tests — is
|
||||
already landed as `src/core/identity/node.rs`),
|
||||
`SouveraineOS/docs/provisioning-gaps.md` (evidence log of everything
|
||||
hand-filled on 2026-07-16 that the ceremony must close),
|
||||
STORAGE-ENCRYPTION.md §Federation (per-node key wrapping rides on
|
||||
commissioning; the pre-split seed backup is moot — the phone should get
|
||||
a *node* key via the ceremony, not a restored seed). Open decisions the
|
||||
doc defers: commissioning authority, CSR format, RedFlag attestation.
|
||||
It's a full sibling arc — start it fresh, not as a tail.
|
||||
|
||||
## Operational notes (learned the hard way)
|
||||
|
||||
- **archdev root was 100% full** — caused lld SIGBUS ("Bus error") that
|
||||
looked like a linker bug. Freed to ~5-10G by wiping the stale
|
||||
souveraine target and `pacman -Sc`. `~/builds` (35G) and `~/build`
|
||||
(14G) are Casey's infra — untouched. Watch disk before big builds.
|
||||
- Souveraine work tree on archdev: `~/Projects/souveraine-fable`
|
||||
(rsynced from the laptop repo; build with
|
||||
`CARGO_TARGET_DIR=$HOME/Projects/souveraine/target`, cross via
|
||||
`SOUVERAINE_AARCH64_SYSROOT=$HOME/aarch64-sysroot ./scripts/build-cross.sh
|
||||
--features secrets --bin souveraine-secrets`). Player builds in
|
||||
`~/souveraine-player` on archdev. NEVER build on the laptop.
|
||||
- rsync with ABSOLUTE source paths — a cwd reset mid-session once sent
|
||||
`/home/casey` (browser cache included) toward archdev and filled the
|
||||
disk. Cleaned.
|
||||
- gnome-keyring's two old items (`login/1`, `login/5`) were
|
||||
`gkr:compat:hashed:*` — attributes one-way hashed, structurally
|
||||
non-portable; owning app (Chatty by shape) re-creates on next login.
|
||||
- The three-tier caller-identity model (Flatpak app id / Waydroid binder
|
||||
UID / per-agent UNIX accounts) is the gating track's foundation —
|
||||
recorded in STORAGE-ENCRYPTION.md §App permission gating; nothing this
|
||||
session forecloses it.
|
||||
|
|
@ -1,5 +1,22 @@
|
|||
# TASK 11 — souveraine-secrets on the phone
|
||||
|
||||
**Status: COMPLETE (2026-07-20).** `souveraine-secrets` owns
|
||||
`org.freedesktop.secrets` on the phone from boot, reboot-verified
|
||||
(a secret stored pre-reboot decrypted after); gnome-keyring masked and
|
||||
not running; `secret-tool` round-trip green. Both blockers resolved:
|
||||
the seed question dissolved — the store key now roots in a machined
|
||||
signature (verified `source=Machined` on-device), no seed restore
|
||||
needed; keyring masked by the idempotent deploy script
|
||||
(`souveraine/packaging/deploy-secrets-phone.sh`). Went beyond the
|
||||
acceptance bar: items sealed AES-256-GCM, Argon2id passphrase wrap
|
||||
slot enrolled via `org.souveraine.Secrets.Manage` (ingress pending —
|
||||
stepUp dialog). Design of record: `docs/STORAGE-ENCRYPTION.md`.
|
||||
Session detail: `docs/handoff-2026-07-20-secrets.md`.
|
||||
|
||||
---
|
||||
|
||||
Original task follows.
|
||||
|
||||
**Status:** built. Two blockers before it can run.
|
||||
|
||||
## Goal
|
||||
Loading…
Reference in a new issue