Sukrara (alpha)

06 — Sharing, follower apps and key management without lock-in

2026-10-16 · investigation report · follows 03 — Nostr · spec: PROTOCOL-v2

Goal

Result

All of it works on emulators, end to end, across two independent implementations. A non-technical flow (scan a QR → both screens show the same 4-digit code → Allow) shares glucose without any secret ever being shown; removing a follower rotates the key while everyone else keeps going; a new phone restores everything from one password-manager entry and followers notice nothing. Real-phone tests with Proton Pass / Bitwarden are the remaining step.

Context

03 proved Nostr carries the data. It left the user-facing parts open, and the user asked for them:

  1. Follower apps, not just the Mac menubar — starting with Android.
  2. Password-manager integration for the keys — at least Proton Pass and Bitwarden — without lock-in (the user was explicitly worried that passkeys can’t be moved between managers).
  3. A non-technical way to share glucose with someone, and to stop sharing.
  4. Key rotation that keeps everything working without anyone re-doing setup.
  5. One app for both roles (closer to the final product) — without false positives: a follower must never look like it works because it reads the publisher’s local data.

Findings that shaped the design (research, with sources in the appendix)

Topic Finding Consequence
Password managers Android Credential Manager password items work with Proton Pass (≥ 1.32), Bitwarden, Google Password Manager, KeePassDX on Android 14+; no domain / assetlinks.json needed; no length limit that matters Back up the identity key as a password item
Proton Pass quirk Returns a saved item only to an app with the same signing certificate Sign every build with one key
Passkeys Need a domain + assetlinks.json; Bitwarden Android doesn’t return PRF; Proton Pass has no passkey export (CXP) yet → a passkey in Proton Pass can’t move to Bitwarden today No passkeys for now — exactly the lock-in the user feared
Group crypto (MLS / Marmot) Mature-ish but every member must process every commit in order; followers (Mac, Home Assistant) can be offline for days Not used: one shared read key, delivered privately per follower (gift wrap), rotated on revoke — the pattern of the open NIP-DA proposal (nips#2411)
Delegation / key migration NIPs NIP-26 unrecommended; all migration NIPs unmerged App-defined: identity key signs a roster of device keys
The 03 QR It contained the read key → a screenshot = permanent access v2 invites contain no secrets

Design (PROTOCOL v2)

 identity key I ──(password manager backup)          follower F (own key, per device)
   │ signs roster: devices=[D], epoch n                  ▲
   │ seals key deliveries ──── gift wrap (NIP-59) ───────┘  {sid, epoch n, read key G_n, devices}
 device key D (phone) ── signs live + chunks, encrypted to G_n ──► relays ──► followers (accept only roster devices)

Reference implementation + end-to-end tests (Python, real public relays)

spikes/nostr/v2/: cgm2.py (protocol), nodes.py (owner / follower), scenarios.py. Every node has its own keys, own SQLite and own relay connections — the only shared thing is the public relays, so a follower can only show data that really travelled through Nostr. Result: 19/19 checks pass (results/v2-scenarios.txt):

# Scenario Result
1 Share: request → approve → 12 h backfill + live ✅ approved in ~1 s; 724/724 readings identical to the owner’s; every row records the relay it came via
2 Negative: owner denies a request ✅ no key, 0 readings
3 Negative: forged key delivery (sealed by an attacker) ✅ rejected — not sealed by the identity key
4 Negative: reading signed by a non-roster key ✅ never delivered (subscriptions are roster-filtered) and rejected if a relay delivers it anyway
5 Revoke follower A while B keeps working ✅ B gets epoch 2 and continues; A gets “access removed”, 0 new readings, its rekey requests refused
6 Phone change (restore identity only) ✅ followers + stream restored from the relay backup; B continues on the new phone without re-inviting; readings signed by the old phone are rejected
7 Follower missed a rotation ✅ asks for the key, gets it, recovers

Bugs these tests caught (fixed, and written into the spec):

Android app — “CGM Share”

spikes/nostr/android-app (dev.spike.cgmshare, Kotlin, ~3.6k lines). One app, two roles picked at onboarding — “I wear the sensor” / “I follow someone” — with separate databases, separate Keystore keys and separate state files; only the chosen role’s engine is ever created.

Share QR Approve dialog with match code Follower home Save to password manager

Tested on two emulators = two separate devices (and against the Python reference); evidence in android-app/evidence/:

Scenario Result
Share → join → approve request reached the owner in 1.2 s; follower 720/720 readings identical to the owner’s 12 h
Negative: owner denies follower stays “waiting”, 0 readings
Negative: forged key delivery rejected: “not sealed by the identity key”
Negative: insider who knows the read key publishes live/chunk/roster from a non-roster key 4× “author not in roster”, 1× “roster not signed by identity”, 0 forged values stored (with relay-side author filtering deliberately disabled to exercise the app’s own rule)
Revoke A, B keeps going A got “access removed” in 0.2 s and nothing after; B (Python follower) moved to epoch 2 and continued
Phone change cleared owner app data → restored from a password manager (KeePassDX) → same identity, new device key, epoch 3; app follower + Python follower continued without re-inviting, 719/719 identical, no gap
Password manager KeePassDX as the emulator’s credential provider: Save and Restore work through the same API Proton Pass and Bitwarden implement
Cross-implementation app follower ↔ Python owner: 770/770 readings equal; Python follower ↔ app owner: same match codes, epochs 1→2→3
Alarms “HIGH glucose” and “No glucose data” notifications fired

Bugs the tests caught (fixed):

Battery-relevant choices: no polling, no WebSocket pings; owner wakes briefly per reading and checks for access requests when it publishes a chunk (~5 min); the follower keeps one inexact, idle-allowed alarm re-armed to “last reading + 6 min”, so it never fires while data flows.

Not done: Proton Pass / Bitwarden on the real phone (needs the user’s accounts), camera decoding on a real camera (only proven via “from image”), restart after reboot, real-phone Doze. Emulator latency numbers are inflated.

How the “false positive” worry is handled

  1. Owner and follower are separate devices in every test (two emulators; Python nodes with separate storage).
  2. The follower role has its own database and keys; each stored reading records which relay it came from.
  3. Negative tests must fail closed: denied requester, forged key, non-roster author, revoked follower — all show no (new) data.
  4. Two independent implementations (Kotlin app, Python reference) agree reading-for-reading in both directions.
  5. The worry was justified: testing found a real one (stale data after “Stop following”) — fixed.

Open questions / next

Appendix: sources