Skip to content

feat: encrypted seed export to Nostr + Blossom (WalletExport)#894

Draft
DavidGershony wants to merge 1 commit into
mainfrom
feat/cloud-seed-backup
Draft

feat: encrypted seed export to Nostr + Blossom (WalletExport)#894
DavidGershony wants to merge 1 commit into
mainfrom
feat/cloud-seed-backup

Conversation

@DavidGershony

Copy link
Copy Markdown
Collaborator

Summary

Opt-in cloud backup of the wallet seed protected by a user-chosen Recovery Passphrase. Two layers of encryption (NIP-44 outer + AES-256-GCM inner with Argon2id KDF), encrypted blob stored on ≥2 of 4 Blossom servers, kind 30078 manifest event makes it discoverable from the passphrase alone — no account, username, or email.

  • Backup: passphrase → Argon2id → HKDF → (backup_sk, backup_pk, K_inner). Seed JSON encrypted with K_inner, uploaded to Blossom (BUD-02 kind-24242 auth signed by backup_sk, server hash verified). Manifest NIP-44-encrypted to backup_pk and published as a parameterized-replaceable kind 30078 under the same npub.
  • Recovery: passphrase → derive backup_pk → query relays for the manifest → fetch blob from any healthy Blossom server → AEAD-decrypt → seed in hand.

What's in this PR

  • Angor.Sdk.WalletExport.Crypto — Argon2id (OWASP m=64MiB/t=3/p=1), AES-256-GCM, HKDF domain-separated secp256k1 backup keys, NIP-44 self-ECDH envelope. Secrets zeroed on dispose.
  • Angor.Sdk.WalletExport.Blossom — BUD-02 upload + content-hash-verified download + HEAD existence probe.
  • CloudBackupService, BackupRecoveryService, WalletCloudBackupService (wallet-store integration via EncryptedWallet.CloudBackup field).
  • 25 unit tests — KDF determinism + NFC normalisation, AEAD tamper/AAD/wrong-key rejection, key-triple distinctness, zero-on-dispose, full two-layer roundtrip including wrong-passphrase + unrelated-keys rejection.
  • DI wired in both Avalonia and design composition roots.
  • Settings UI in the design app — new Cloud Backup card with enable/refresh/verify/disable actions. (Passphrase-entry modal XAML is intentionally a follow-up; the existing wipe-data modal at SettingsView.axaml:1038 is the template.)
  • INetworkConfiguration.GetDefaultBackupServerUrls() added across all three implementations.

Threat model highlights

Threat Mitigation
Relay or Blossom compromise Both layers encrypted; attacker still needs the passphrase
Weak passphrase Argon2id m=64MiB makes each guess ~250ms; UI enforces 12-char minimum
Device compromise Recovery passphrase is not cached locally; in v1 refresh requires re-entering it
Tampered ciphertext AEAD tag + Schnorr signature on the relay event
Blob lost on some servers Health check surfaces N-of-M reachable count; passphrase-gated refresh re-uploads

What's deferred (clean follow-ups)

  1. Passphrase-entry modals for Enable / Refresh in the UI — ViewModel surface is ready (OpenEnableBackupModal, ConfirmEnableBackupAsync, etc.). Copy the wipe-data modal as a template.
  2. "Restore from Cloud Backup" entry in AddWalletFlow.cs:13 — calls IBackupRecoveryService.RecoverAsync(passphrase) then existing IWalletAppService.CreateWallet(...).
  3. Silent refresh-on-launch with cached backup_sk — needs a new platform-abstract secure-key store. v1 is more secure (passphrase required for refresh).
  4. Avalonia production app UI — pre-existing net9.0 (Avalonia) vs net10.0 (SDK) framework mismatch in the repo prevents that app from building; unrelated to this feature.

Test plan

  • dotnet test src/sdk/Angor.Sdk.Tests --filter Angor.Sdk.Tests.WalletExport — all 25 unit tests pass
  • Open Settings in the design app on a fresh wallet — Cloud Backup card shows "not set up"
  • Click "Set up Cloud Backup" → ViewModel opens enable modal (modal XAML deferred; covered by follow-up)
  • With backup enabled, click "Verify Health" → status line shows "Reachable on N of M servers"
  • Manual end-to-end against real relays + Blossom: encrypt a known seed, click Disable, then run the recovery flow via the SDK service to confirm the seed comes back identical

🤖 Generated with Claude Code

Adds an opt-in cloud backup of the wallet seed under a user-chosen
Recovery Passphrase. Two-layer envelope:
  inner = AES-256-GCM keyed by Argon2id(passphrase) → HKDF
  outer = NIP-44 v2 self-encryption to a passphrase-derived backup npub

The encrypted blob is uploaded to ≥2 of 4 Blossom servers (BUD-02
kind-24242 auth signed by the passphrase-derived nsec, content
verified by SHA-256). A kind 30078 manifest event under the same
npub makes the backup discoverable from the passphrase alone — no
account, no username.

Recovery: passphrase → derive npub → query relay for manifest →
fetch blob from any healthy Blossom server → AEAD-decrypt seed.

SDK:
 - Angor.Sdk.WalletExport.Crypto: Argon2id, AES-GCM, HKDF-derived
   secp256k1 backup keys, NIP-44 self-ECDH envelope. Secrets zeroed
   on dispose.
 - Angor.Sdk.WalletExport.Blossom: BUD-02 upload + content-hash
   verified download + HEAD existence probe.
 - CloudBackupService / BackupRecoveryService: orchestration.
 - WalletCloudBackupService: persists CloudBackupRecord onto
   EncryptedWallet for status + passive health checks.

Tests:
 - 25 unit tests cover KDF determinism + NFC, AEAD tamper rejection
   + AAD enforcement, key-triple derivation + domain separation +
   zero-on-dispose, and full two-layer roundtrip including
   wrong-passphrase + unrelated-keys rejection.

UI (design app):
 - Settings → new Cloud Backup card with enable/refresh/verify/
   disable actions. ViewModel surface is ready; passphrase-entry
   modal XAML is a follow-up (existing wipe-data modal is the
   template).

DI wired in both Avalonia and design composition roots; webapp
NetworkConfiguration extended with GetDefaultBackupServerUrls().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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