Files
zapstore/spec/work/WORK-011-device-key.md
T

8.4 KiB

WORK-011 - Device Key (Phase A + B + D)

Feature: FEAT-006-device-key.md Status: Complete

Tasks

  • 1. Create DeviceKeyService
    • Files: lib/services/device_key_service.dart
    • Generates/loads key from FlutterSecureStorage
    • Exposes devicePubkeyProvider (StateProvider)
  • 2. Register device signer at boot
    • Files: lib/main.dart
    • In storageReadyProvider, after SQLite init
    • signIn(setAsActive: false), sets devicePubkeyProvider
  • 3. Device key UI in profile screen
    • Files: lib/screens/profile_screen.dart
    • Shows truncated npub, copy private key with warning dialog
  • 4. Rewrite bookmarks to use device key
    • Files: lib/services/bookmarks_service.dart, lib/widgets/app_detail_widgets.dart, lib/widgets/floating_overflow_menu.dart
    • Changed from FutureProvider to synchronous Provider
    • No manual nip44Decrypt calls (EncryptableModel auto-decrypts)
    • No sign-in gate; always available
  • 5. Rewrite unmanaged apps to use device key
    • Files: lib/services/unmanaged_apps_service.dart, lib/services/updates_service.dart
    • Changed from FutureProvider to synchronous Provider
    • No sign-in gate
  • 6. Remove sign-in gate from SaveAppDialog
    • Files: lib/widgets/bookmark_widgets.dart
    • Removed SignInPrompt, uses device signer directly
  • 7. Remove appStackEventFilter, use #h tag filtering
    • Files: lib/constants/app_constants.dart, lib/screens/user_screen.dart, lib/widgets/app_stack_container.dart, lib/widgets/bookmark_widgets.dart
    • Removed filter function entirely
    • Public stack queries now use '#h': {kZapstoreCommunityPubkey} tag
  • 8. Migrate Amber private stacks on sign-in
    • Files: lib/services/device_backup_service.dart, lib/services/device_key_service.dart
    • Offers restore before migration so the final device key is chosen first
    • Queries Amber-authored encrypted AppStacks after Amber connection
    • Merges them into device-authored encrypted stacks using the device signer
    • Normalizes legacy installed/unmanaged d-tags to current identifiers
    • Rechecks idempotently on every sign-in; failed decrypts remain retryable
  • 9. Store device key backups in encrypted settings
    • Files: lib/services/device_backup_service.dart
    • Uses device-signed CustomData with d=zapstore-settings
    • Stores NIP-44 Amber recovery capsules authorized by Amber signatures
    • Ignores legacy Amber-signed settings
  • 10. Self-review against INVARIANTS.md
  • 11. Centralize private 30267/30078 signing
    • Device signer only, encrypted-stack h-tag rejection, 16-bit NIP-13
    • Purplebase worker executor keeps mining off Flutter's main isolate
  • 12. Add boot-only private event synchronization
    • One cancellable stream=false request for device-authored 30267/30078
    • Private consumers use LocalSource only
  • 13. Replace Amber-owned settings backup
    • Device-signed zapstore-settings with Amber recovery capsules and p tags
    • Ignore all legacy Amber-signed settings
  • 14. Repair Amber migration
    • Explicitly decrypt Amber stacks before merge
    • Cover bookmarks, installed, unmanaged, and arbitrary encrypted stacks
    • Recheck idempotently on every Amber sign-in
  • 15. Move trusted-signers CustomData to device ownership
  • 16. Consolidate bookmark writes and private stack reads
  • 17. Add lifecycle, failure, migration, and PoW tests

Test Coverage

Scenario Expected Status
Device private write Device author, valid PoW, local save [x]
Invalid private policy Unknown 30078 and encrypted h-tag rejected [x]
PoW cancellation Worker terminates and tags remain unchanged [x]
Boot sync lifecycle Single-flight and cancellable [x]
Bookmark write overlap Serialized monotonic replacements [x]
Bookmark save failure Optimistic state rolls back [x]
Amber bookmark migration Imperative result explicitly decrypts [x]
Recovery injection Amber authorization required [x]
Recovery cancellation No work restarts after cancellation [x]
Full verification App, models, and Purplebase suites pass [x]

Decisions

2026-05-07 - Migrate Amber-authored private stacks

Context: Users may have bookmarks encrypted to their Amber key. Decision: On Amber connection, migrate encrypted AppStacks authored by the Amber pubkey to equivalent device-key stacks. Rationale: Private data should follow the new device-key ownership model without losing existing saved apps, installed-app backups, or unmanaged app state.

2026-05-07 - Synchronous providers for bookmarks/unmanaged apps

Context: Previously FutureProvider because of manual decrypt. Now EncryptableModel auto-decrypts. Decision: Changed to synchronous Provider<Set>. Rationale: EncryptableModel.prepareAfterLoading runs in RequestNotifier before emission. By the time the provider reads the stack, privateAppIds is already decrypted.

2026-05-07 - #h tag replaces schemaFilter

Context: appStackEventFilter rejected encrypted stacks and stacks without app refs. Decision: Removed entirely. Public stacks are identified by having '#h': {communityPubkey} tag. Rationale: Per invariant "Encrypted stacks MUST NOT include a community h tag", filtering by #h naturally excludes all private stacks.

2026-05-07 - Device backups live inside settings

Status: Superseded by the 2026-07-13 device-settings decision below. Context: Device key backup needs to be tied to the user's encrypted settings event, not a separate replaceable event. Decision: Store backup entries inside the Amber-signed CustomData event with d=zapstore-settings, under the deviceBackups JSON key. Rationale: Keeps backup state with settings and avoids a standalone zapstore-device-backup event. The entire settings JSON object is NIP-44 encrypted before signing.

2026-07-13 - All private events are device-owned

Context: Amber ownership made private state depend on the current identity and allowed private write paths to drift. Decision: Every private kind 30267/30078 event is device-signed. Public 30267 stacks remain Amber-signed and carry the community h tag. Rationale: The device key is always available and is the stable owner of per-device state.

2026-07-13 - Device settings contain recovery capsules

Context: Amber must recover a device nsec without authoring the settings event or owning device settings. Decision: zapstore-settings is signed by the device key. Its versioned envelope stores device-private data plus NIP-44 capsules addressed to Amber keys and indexed by p tags. Capsules include an embedded Amber-signed authorization binding the Amber identity to the device pubkey. Rationale: Settings remain per-device while Amber remains an orthogonal recovery recipient, while copied or attacker-created capsules cannot inject a device key that Amber never authorized.

2026-07-13 - Boot-only relay ingestion

Context: Private state changes originate on the device and live subscriptions waste relay and lifecycle resources. Decision: Fetch device-authored private events once at boot with stream:false; UI providers are local-only. Amber recovery/migration is the only sign-in-time one-shot exception. Rationale: Preserves local-first rendering and avoids duplicate or leaked subscriptions.

2026-07-13 - 16-bit off-isolate proof of work

Context: Private events need NIP-13 without running CPU-bound mining on Flutter's main isolate. Decision: Apply 16-bit PoW through a cancellable Purplebase worker executor. Rationale: 16 bits is a practical mobile spam cost; worker execution keeps rendering responsive.

Spec Issues

None

Progress Notes

2026-05-07: Phase A, B, and D complete. Migration added for Amber-authored private stacks. Analysis could not be rerun in this sandbox because fvm and pub-cache reads are blocked.

2026-05-07: Restore ordering hardened: if an Amber backup contains other device keys, the restore/keep-current choice happens before migration and backup. Migration completion is keyed by both Amber pubkey and final device pubkey, and empty migration results are left retryable.

2026-05-07: Device key backup moved into the encrypted zapstore-settings CustomData event. No zapstore-device-backup event is written.

2026-07-13: FEAT-006 was updated by explicit product authorization. The new contract supersedes the earlier Amber-signed settings decision and ignores legacy Amber settings even when that loses old backups.