Companion 0.5.28 — backup & restore, NIP-46 remote signer, companion-gated install pitch #149

Merged
lfg2025 merged 10 commits from companion/0.5.28-ship into main 2026-08-31 18:38:44 +00:00
Owner

The companion-agent queue from the 2026-08-30 handoff, complete:

  • Backup & Restore (#128): hub sub-page, SAF export/import sealed in the node's ADR-005 envelope (Argon2id + ChaCha20-Poly1305, byte-compatible with core backup.rs), merge-only restore, no cloud.
  • Remote Signer (#139): the phone is the NIP-46 bunker — nsec generate/import, nostrconnect:// QR pairing (scanner + OS deep link), per-request approve/deny card, NIP-44 v2 transport with NIP-04 receive fallback, wire-faithful to rust-nostr's reference bunker. Crypto pinned to the official NIP-44 + BIP-340 vectors; e2e harness nip46-test-client.py included.
  • #61 residual: banner + manual intro trigger + overlay all gate on isCompanionApp() (web-side, vitest-covered).
  • Hub modal: sub-pages like Nodes/FIPS, 70% height cap, node ULA display/copy, fipssh Termux helper (npub→ULA is a pure public-key function, verified against the fips crate).

Served artifact refreshed: archipelago-companion.apk v0.5.28/vc48 + meta json (clean build, v1+v2+v3 signed and verified). Issues #61/#128/#139 updated on the tracker.

Companion 0.5.28 (vc48) — testing checklist: in-place upgrade from 0.5.27; hub → Backup & Restore / Remote Signer; signer e2e via the Python harness; QR decision-trigger (move-to-the-code) for the shelved zxing-cpp option.

The companion-agent queue from the 2026-08-30 handoff, complete: - **Backup & Restore (#128):** hub sub-page, SAF export/import sealed in the node's ADR-005 envelope (Argon2id + ChaCha20-Poly1305, byte-compatible with `core backup.rs`), merge-only restore, no cloud. - **Remote Signer (#139):** the phone is the NIP-46 bunker — nsec generate/import, `nostrconnect://` QR pairing (scanner + OS deep link), per-request approve/deny card, NIP-44 v2 transport with NIP-04 receive fallback, wire-faithful to rust-nostr's reference bunker. Crypto pinned to the official NIP-44 + BIP-340 vectors; e2e harness `nip46-test-client.py` included. - **#61 residual:** banner + manual intro trigger + overlay all gate on `isCompanionApp()` (web-side, vitest-covered). - **Hub modal:** sub-pages like Nodes/FIPS, 70% height cap, node ULA display/copy, `fipssh` Termux helper (npub→ULA is a pure public-key function, verified against the fips crate). Served artifact refreshed: `archipelago-companion.apk` v0.5.28/vc48 + meta json (clean build, v1+v2+v3 signed and verified). Issues #61/#128/#139 updated on the tracker. **Companion 0.5.28 (vc48) — testing checklist:** in-place upgrade from 0.5.27; hub → Backup & Restore / Remote Signer; signer e2e via the Python harness; QR decision-trigger (move-to-the-code) for the shelved zxing-cpp option.
lfg2025 added 10 commits 2026-08-31 18:38:06 +00:00
Only the auto-popup was companion-gated — inside the companion WebView
users still saw the 'install the companion' banner in the App Store and
could pop the intro overlay through it. Gates all three paths on
isCompanionApp(): CompanionBanner self-hides, openCompanionIntro() is a
no-op, and the manual-open watcher in CompanionIntroOverlay refuses to
open (the overlay's raw window check also moves to the canonical helper
so there is exactly one detection). No APK change.
The QR-decoder option doc was written on an offline machine with the
Maven coordinates and wrapper API flagged as from-memory. Verified
against Maven Central + the wrapper source: artifact is
io.github.zxing-cpp:android:3.1.1 (current release), Format.QR_CODE is
nested inside BarcodeReader (not a top-level BarcodeFormat), options are
a constructor-argument data class, and read(ImageProxy) handles the
Y-plane/cropRect/rotation itself. Sketch updated accordingly; the option
itself stays NOT-actioned pending the move-to-the-code decision trigger.
Extends archy-fips-core with the two companion-release features' crypto
(#128, #139), same JNI-over-JSON contract as the mesh surface:

backup.rs — the ADR-005 encrypted-backup envelope, byte-compatible with
the node's backup code (Argon2id default params + ChaCha20-Poly1305,
blob = base64(salt||nonce||ct)); decrypt ignores extra envelope fields
so node backups read here too. Round-trip, tamper, wrong-passphrase and
cross-shape tests included.

nostr.rs — the phone-side remote-signer crypto: nsec/npub bech32 keys,
BIP340 schnorr event signing (NIP-01 id serialization), NIP-44 v2
payloads (HKDF-SHA256 + ChaCha20 + HMAC-SHA256, both padding prefixes),
NIP-04 fallback, nostrconnect:// parsing with repeated relay params.
Verified against the official NIP-44 vectors (conversation keys, message
keys, padded lengths, byte-exact encrypt vectors), the BIP-340 reference
sign vectors, and round-trip/tamper/failure tests. secp256k1 0.29 note:
Keypair::public_key() is the 33-byte compressed key — x-only pubkeys
must go through .x_only_public_key().0 (one real bug the vectors caught).

JNI glue adds com.archipelago.app.NativeCore: backupEncrypt/Decrypt,
nostrGenerateSecret/SecretFromAny/ParseConnectUri/SignEvent and the
NIP-44/NIP-04 cipher pairs. Android arm64 build verified via cargo-ndk
(7.2 MB .so, +0.4 MB for both modules). Host: cargo test 23/23, clippy clean.
Companion 0.5.28 (versionCode 48), the companion-agent queue items:

#128 Backup & Restore — the phone side of losing your phone or wiping it
to cross a border. Hub card → SAF export/import of an encrypted .json:
everything the app holds (servers+passwords, FIPS identity/peers, signer
key) sealed in the node's ADR-005 envelope (Argon2id + ChaCha20-Poly1305,
native backup.rs — same blob layout as the node's, node-shaped envelopes
decrypt too). Restore is merge-only: servers upsert npub-first, identity
and signer key adopt only when absent, peers union by npub. No cloud, no
telemetry — the file goes wherever the user saves it.

#139 Remote Signer — the phone IS the NIP-46 bunker. Generate/import a
nostr key, scan a nostrconnect:// QR (in-app scanner or deep link), and
approve/deny each sign_event request from a legible card (kind label,
content, tags, time) — nothing signs without a human. Wire-faithful to
rust-nostr's reference bunker (connect-carrying-secret handshake, NIP-44
v2 transport with NIP-04 receive fallback, kind-24133 responses);
get_public_key/describe/ping handled, everything else 'not authorized'.
Session state in BunkerManager, UI in SignerScreen, hub card wired.

Plus NativeCore (JNI object for the new native surface), FipsPreferences
peers-merge for restore, nostrconnect:// intent filter, and the release
docs (companion-backup-restore.md, companion-nip46-remote-signer.md).

Also Android/tools/nip46-test-client.py: a pure-Python NIP-46 client that
plays the node's login role (QR, handshake, get_public_key, sign_event)
and verifies the phone's signature with an independent BIP-340 — the
end-to-end test for the feature until node-side lands. Its crypto matches
the official NIP-44 + BIP-340 vectors byte-for-byte, the same vectors the
Rust core passes, so the two interop by construction.

Built + smoke: assembleDebug v0.5.28/vc48, same signing cert as the
served 0.5.27 (d622e07e…644d) so it updates in place.
Field feedback on 0.5.28: the standalone Backup/Signer screens were hard
to read over the synthwave background, back left the app instead of the
menu, and they broke the hub's one-container interaction model. Both are
now hub sub-pages exactly like Nodes/FIPS:

- BackupSection / SignerSection (ui/components) render inside the NESMenu
  panel with the menu's own dark glass surface, scrim, and palette — the
  readability and theming problem disappears with the standalone surface.
- The header back arrow returns to the hub card page (same as Nodes).
- The panel height cap drops from 92% to 70% of the screen — ~15%
  breathing margin top and bottom; content scrolls inside.
- The pairing QR scanner is hosted by NESMenu OUTSIDE the panel
  (QrGlassModal is a full-screen Box, not a Dialog — inside the panel's
  scroll it would clip), and decoded nostrconnect:// URIs funnel into the
  signer section through the same latch as the deep link.
- The nostrconnect:// deep link now routes to the session and pops the
  hub open on the signer sub-page (SignerLaunch singleton) instead of a
  dedicated route; standalone screens and routes removed.
- BunkerManager.refreshState is now a proper suspend fun (was
  runBlocking on the caller's dispatcher).

Docs updated to the new locations. Rebuilt for on-device testing
(v0.5.28-debug/vc48, same signing cert).
FIPS nodes carry their fips0 ULA in the saved entry, but it was never
displayed — the only way to learn it was the node itself. Each FIPS
node row in the Nodes page now shows its mesh address as a monospace
subtitle with a tap-to-copy affordance, which is exactly the address
other apps on the phone (Termux ssh over the split tunnel, for
example) need. Non-FIPS entries are unchanged.
Today's field test: Termux over the companion's split tunnel reaches the
node's fips0 ULA and gets RST — the mesh path works end to end, port 22
is refused by the node (fips0 default-deny, no 22 in the fips.d drop-ins;
sshd IPv6 listening unverified). The interim manual unblock (a
source-restricted 90-ssh.nft drop-in) is documented, but the real ask is
a first-class 'SSH over mesh' settings toggle in the node UI, with the
drop-in lifecycle owned by the daemon, a source-scope decision (paired
phones vs any mesh peer), sshd preflights, and an acceptance checklist.
Companion side is done (device-wide split tunnel + the node ULA now
displayed/copyable in the hub Nodes page) — the node agent is downstream.
Verified against the fips crate source: the mesh ULA is a pure function
of the PUBLIC key — fd || sha256(x-only pubkey)[0..15] — so the npub is
the durable address and needs no resolver. Android/tools/fipssh wraps
ssh for Termux: 'fipssh user@npub1…' derives the ULA (pure-python
bech32 + sha256, checksum-validated, typo protection) and execs ssh
over the companion's split tunnel; --resolve prints the ULA alone.

The derivation is pinned by a new Rust test
(npub_derives_the_same_mesh_ula_as_the_fips_identity, 3 seeds against
fips::Identity) and the helper's output was verified byte-identical
against a live fips identity pair. SSH-over-mesh handover updated with
an addendum: node docs/UI can advertise npub-based addressing, no
node-side DNS needed for this case.
The companion-agent queue from the 2026-08-30 handoff, complete:

- Backup & Restore: hub sub-page, SAF export/import sealed in the
  node's ADR-005 envelope (Argon2id + ChaCha20-Poly1305, byte-compatible
  with core backup.rs), merge-only restore, no cloud.
- Remote Signer: the phone is the NIP-46 bunker — nsec generate/import,
  nostrconnect:// QR pairing (scanner + OS deep link), per-request
  approve/deny card, NIP-44 v2 transport with NIP-04 receive fallback,
  wire-faithful to rust-nostr's reference bunker. Crypto pinned to the
  official NIP-44 + BIP-340 vectors; e2e harness included.
- #61 residual: banner + manual intro trigger + overlay all gate on
  isCompanionApp() (web-side, vitest-covered).
- Hub modal: new sub-pages like Nodes/FIPS, 70% height cap, node ULA
  display/copy in the Nodes list, fipssh Termux helper (npub→ULA is a
  pure public-key function — verified against the fips crate).

Issues #61 (comment), #128, #139 closed on the tracker.
lfg2025 merged commit 9f1a289d1a into main 2026-08-31 18:38:44 +00:00
Sign in to join this conversation.