Skip to content

feat: opt-in encrypted config backup - #81

Merged
deymosh merged 7 commits into
masterfrom
feat/config-backup
Oct 1, 2026
Merged

deymosh merged 7 commits into
masterfrom
feat/config-backup

Conversation

@deymosh

@deymosh deymosh commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

An opt-in backup of the phone's setup, so a new or reinstalled phone with the same identity picks up where the old one left off.

  • What is saved: only what the phone alone knows. That is each paired machine's relays, label, direct endpoints and new-session defaults; the settings; the quick prompts; and the session-key ring (secrets and expiries) with each machine's confirmed grant. What a bridge re-sends in its heartbeat is left out.
  • How: one NIP-78 event (kind 30078) on a relay the user picks. The content is NIP-44 encrypted to the identity through the signer, so NIP-55 logins work too. The d tag is a hash of the identity and an app-private context, so it names neither the app nor the content. The backup relay joins the relay set, so the Tor and NIP-42 rules apply to it.
  • When: 30 s after the last change worth backing up, only if the content changed, or on "Back up now". Turning it on looks on the relay first. A backup already there waits for Restore or Keep this phone's, and nothing is saved over it meanwhile.
  • Restore merges. Missing machines are added, existing ones keep their own setup, and settings and quick prompts become the backup's. The backup's session keys are adopted only while the phone has granted none of its own, so a restored phone reads its bridges at once without a signer prompt per bridge. A session key never signs, so it cannot command a bridge, and only the identity can open the backup.
  • Off: turning backup off can also send a NIP-09 deletion.
  • Android:
    • a Settings → Backup page;
    • a one-time restore step right after logging in with an existing key;
    • Account → "Show" for the nsec of an on-device key, read off the main thread and copied as sensitive.

Commits

  • client-core: backup payload, d tag, fingerprint, total decode, merge
  • nostr-transport: #d filters
  • client-runtime: seal/open/delete events, the save/fetch/import loop, intents, a two-phone round-trip test
  • client-core: session keys + grants in the backup (adopt-only-if-unused rule, tests)
  • client-ffi: intents, UniffiBackupView, nsec_of; bindings regenerated
  • android: Backup page, restore step, show key; snapshots
  • docs: CLIENT.md section

Test plan

  • cargo clippy --workspace --all-targets -D warnings, cargo test --workspace
  • agent-host typecheck + test
  • Android testDebugUnitTest verifyPaparazziDebug (new goldens reviewed)
  • Device test of a two-phone restore (maintainer, with the prerelease)

🤖 Generated with Claude Code

deymosh and others added 7 commits September 30, 2026 23:07
A bridge knows a phone by its identity pubkey and a phone mints fresh
session keys on its own, so a new phone with the same identity needs
only what the phone alone knows to carry on: each paired machine's
relays, label, direct endpoints and new-session defaults, the settings
and the quick prompts. `ConfigBackup` holds exactly that; everything a
heartbeat re-sends, session keys and grants are left out.

The backup is a NIP-78 kind 30078 event whose `d` tag is a sha256 of
the identity and a context only this app uses, so it names no app on
the relay and no other app's data can share it. Decoding is total and
drops relays the phone may not dial; importing adds the machines the
phone lacks, leaves the ones it has as they are, and takes the
backup's settings and quick prompts. The backup relay is kept under its
own key, apart from the settings the backup carries.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A subscription can now ask for addressable events by their `d` tag
(`#d`), so a client fetching one of its own (say, a backup stored as
application data) asks the relay for just that one instead of every
event of the kind.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The phone can keep its configuration on a relay the user picks. The
backup relay joins the set the transport dials, so it goes through Tor
when that is on and answers the relay's AUTH with the identity, like
every other relay. Its content is NIP-44 encrypted from the identity to
the identity through the identity's signer (an on-device key or a
NIP-55 signer app alike), so only that identity can read it; the relay
sees the pubkey, the kind, when it was saved and an opaque d tag.

Setting the relay looks there first. A backup found waits for the user
to import it (merged into the phone) or keep this phone's, and nothing
is saved meanwhile, so a new phone never overwrites the backup it is
about to restore; with none there, this phone's is saved at once. After
that a backup is saved half a minute after the last change to the
machines, settings or quick prompts, only when what it holds changed,
each one dated past the last. Turning it off can also send a NIP-09
deletion. The settings view carries where the backup stands.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The backup now holds the phone's session-key ring (secrets and expiries)
and each machine's confirmed grant. A restored phone takes them while it
has granted none of its own keys, so it reads what its bridges encrypt to
those keys right away instead of asking the identity's signer to grant
every bridge again. A phone that already granted its own keys keeps them,
and a backed-up grant is restored only if it names a live key of the ring
the phone ends up with.

A session key never signs, so reading it cannot command a bridge, and the
backup is NIP-44 encrypted to the identity, which reads every payload
anyway. The ring's Debug output shows no secret. A rotation or prune of
the ring now also schedules a backup.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Five intents (set the backup relay, import the backup found on it, keep
this phone's configuration instead, back up now, turn it off with an
optional deletion), the backup's relay, last save and status in the
settings view, and nsec_of for a "show my key" on an on-device login.
The Kotlin bindings are regenerated.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Settings gets a Backup page: off, it takes a relay; on, it says when it
last saved, offers a save now and turning it off (with or without asking
the relay to delete the backup); a backup already on the relay waits for
Restore or Keep this phone's, the latter confirmed since it replaces the
backup. Right after logging in with an existing key, a restore step
offers to look on a relay before the app opens; Skip turns backup off
again. Account can show and copy the nsec of a key kept on the phone,
read off the main thread and copied as sensitive so the clipboard preview
hides it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
@deymosh
deymosh merged commit 8f59e46 into master Oct 1, 2026
6 checks passed
@deymosh deymosh mentioned this pull request Oct 1, 2026
@deymosh
deymosh deleted the feat/config-backup branch October 2, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant