From 119442293a6e7cf74c137e6f824e12e98040782c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 23:22:45 +0000 Subject: [PATCH] feat(napplet): trust-boundary core for sandboxed nsite/napplet rendering Adds the platform-agnostic core for hosting untrusted napplet (NIP-5D) / nsite (NIP-5A) web content behind a hard trust boundary, so applet HTML/JS can never reach the nsec, app storage, or LocalCache. The Android host runs the WebView in a separate OS process (:napplet) that holds no secrets and brokers every dangerous operation over IPC to the main process. This commit lands the verifiable heart of that boundary in commons commonMain (KMP-pure, fully unit-tested): - NappletCapability + NAP-domain mapping (default-deny on unknown domains) - NappletIdentity keyed by addressable coordinate (grants survive updates) - NappletPermissionLedger / GrantState / store (persistent vs session vs once; standing DENY is authoritative) - NappletRequest/NappletResponse wire protocol (no response carries key bytes) - NappletBroker: the only holder of the signer; enforces consent, signs as the user only, refuses to publish foreign or unsigned events Architecture, process model, IPC schema, WebView hardening, and consent UX are documented in amethyst/plans/2026-06-19-napplet-sandbox-host.md. The Android :napplet process, WebView host, AIDL broker, and consent UI are the next phase. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde --- .../plans/2026-06-19-napplet-sandbox-host.md | 198 ++++++++++++++++++ .../amethyst/commons/napplet/NappletBroker.kt | 125 +++++++++++ .../napplet/NappletBrokerCollaborators.kt | 54 +++++ .../commons/napplet/NappletCapability.kt | 92 ++++++++ .../commons/napplet/NappletIdentity.kt | 47 +++++ .../commons/napplet/permissions/GrantState.kt | 57 +++++ .../permissions/NappletPermissionLedger.kt | 101 +++++++++ .../permissions/NappletPermissionStore.kt | 69 ++++++ .../napplet/protocol/NappletRequest.kt | 112 ++++++++++ .../napplet/protocol/NappletResponse.kt | 66 ++++++ .../commons/napplet/NappletBrokerTest.kt | 196 +++++++++++++++++ .../commons/napplet/NappletCapabilityTest.kt | 64 ++++++ .../NappletPermissionLedgerTest.kt | 121 +++++++++++ 13 files changed, 1302 insertions(+) create mode 100644 amethyst/plans/2026-06-19-napplet-sandbox-host.md create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBroker.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerCollaborators.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletCapability.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentity.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/permissions/GrantState.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/permissions/NappletPermissionLedger.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/permissions/NappletPermissionStore.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletRequest.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletResponse.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletCapabilityTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/permissions/NappletPermissionLedgerTest.kt diff --git a/amethyst/plans/2026-06-19-napplet-sandbox-host.md b/amethyst/plans/2026-06-19-napplet-sandbox-host.md new file mode 100644 index 0000000000..c39fcf98f1 --- /dev/null +++ b/amethyst/plans/2026-06-19-napplet-sandbox-host.md @@ -0,0 +1,198 @@ +# Napplet / nsite sandbox host — design + +**Date:** 2026-06-19 +**Status:** Draft — core (commons) implemented + tested; Android host specified, not yet built +**Companion:** `quartz/plans/2026-06-19-napplet-nip5a-resolver.md` (the bottom half — manifest parsing + verified Blossom resolution — already landed in `quartz`). + +## Goal + +Render NIP-5A static sites (nsites) and NIP-5D napplets *inside* Amethyst, with +the applet's HTML/CSS/JS running behind a **hard trust boundary**: it must never +be able to read the user's `nsec` or any other secret, read app storage, +`LocalCache`, or other accounts' data, or sign / encrypt / publish / zap without +explicit per-applet user consent. A napplet is untrusted third-party code served +from an untrusted Blossom CDN; we treat it accordingly. + +## Threat model + +**Adversary:** the applet bundle (HTML/CSS/JS), authored by an untrusted party, +delivered from an untrusted CDN. + +It must NOT be able to: + +1. Read the `nsec` / any `NostrSigner`-held secret, or decrypted key material. +2. Read app private storage, `LocalCache`, DataStore, other accounts, or another + applet's sandboxed storage. +3. Sign, encrypt/decrypt, publish, subscribe, or zap without explicit consent. +4. Escalate to native code, other installed apps, or the file system. +5. Reach the network directly to exfiltrate or fingerprint (network is a *brokered + capability*, default-deny). +6. Forge content past the signed manifest (already handled by `StaticSiteResolver`: + manifest is authority, CDN is untrusted, every blob is sha256-verified). + +**Trusted:** the main Amethyst process, the broker, the consent UI, quartz +verification. **Assumption:** the Android System WebView (Chromium) renderer +sandbox is sound — and we add an OS-process boundary on top so that even a full +WebView/renderer escape lands in a process that holds no secrets. + +## Why a separate OS process is the load-bearing decision + +Android processes have isolated address spaces. The decrypted `privKey`, the +`KeyPair`, and the `NostrSigner` instance live **only in the main process heap**. +The applet host runs in a separate process (`android:process=":napplet"`) that: + +- never constructs a `NostrSigner`, never touches `SecureKeyStorage`/Keystore, + never holds an `Account` or `LocalCache`; +- only holds an IPC handle to *request operations*, whose **results carry no key + material** (a signed event, a ciphertext, a pubkey — never the private key). + +So even arbitrary code execution inside the WebView renderer (already its own +sandboxed process) or inside the `:napplet` app process cannot read the main +process's memory where the secret lives. This is the guarantee the same-process +approach cannot make. + +## Process & component model + +``` +┌─────────────────────────────────────────┐ ┌───────────────────────────────────┐ +│ Main process (com.vitorpamplona.amethyst)│ │ Applet process (…:napplet) │ +│ │ │ │ +│ Account · NostrSigner · SecureKeyStorage │ │ NappletHostActivity │ +│ LocalCache · NostrClient · Blossom · NWC │ │ └─ WebView │ +│ │ │ ├─ shell page (trusted, │ +│ NappletBrokerService (bound, not exported)│◀──AIDL─▶│ │ app-asset origin) │ +│ └─ commons NappletBroker │ Binder │ │ exposes bridge → broker │ +│ └─ NappletPermissionLedger │ (UID │ └─