Skip to content

CSPP Cloud Backup iOS Only#615

Draft
praveenperera wants to merge 95 commits intomasterfrom
cspp-cloud-backup-ios
Draft

CSPP Cloud Backup iOS Only#615
praveenperera wants to merge 95 commits intomasterfrom
cspp-cloud-backup-ios

Conversation

@praveenperera
Copy link
Copy Markdown
Contributor

@praveenperera praveenperera commented Mar 18, 2026

Summary

This branch brings the iOS CSPP cloud backup flow to review-ready shape.

On the Rust/core side, it adds the CSPP backup data model, passkey and cloud storage callback interfaces, the new cloud backup manager, encrypted DB change detection, debounced upload handling, startup sync/reconciliation, restore support, and persisted backup verification state.

On iOS, it wires that stack into iCloud Drive and passkeys, adds the iOS 18.4 floor needed for safe PRF behavior, and rounds out the user-facing flows for backup setup, restore discovery, restore progress, backup verification, passkey repair, and cloud-backup detail/status screens. It also hardens recovery behavior around missing passkeys, unsupported backup formats, duplicate-wallet restore cases, and backup integrity verification after wallet changes.

What To Review

  • Enabling cloud backup and creating or recovering the passkey-backed backup state
  • iCloud Drive upload/download/listing behavior and restore discovery
  • Startup restore and catastrophic recovery flows
  • Verification, repair, and reinitialization behavior when backup state becomes stale or inaccessible
  • Sync status and cloud-only wallet handling in settings/detail screens

QA Notes

  • Enable cloud backup on iOS 18.4+ and confirm the initial upload completes
  • Restart after wallet changes and confirm backup state/status re-syncs correctly
  • Test fresh-install restore flow, including progress and partial-failure handling
  • Test passkey-missing / deleted-passkey repair flow
  • Confirm duplicate wallets are skipped safely during restore
  • Confirm newer/unsupported backup formats fail safely without overwriting good data

Closes: #562 #566 #567 #569 #570 #571 #575 #577 #578 #579 #582 #585 #600 #601 #603 #604

Also addresses: #580 #583 #584

@praveenperera praveenperera marked this pull request as draft March 18, 2026 18:26
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 53e2ba37-49b8-490b-a64c-2033ac5618c1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cspp-cloud-backup-ios

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bitcoinppl bitcoinppl deleted a comment from greptile-apps bot Mar 18, 2026
@praveenperera praveenperera force-pushed the cspp-cloud-backup-ios branch from 19603ae to e797c50 Compare March 27, 2026 10:30
Use an independent random key stored in keychain for local DB encryption
instead of deriving it from the CSPP master key. This prevents cloud
backup restore from invalidating the local encryption key and wiping all
local databases.

- Add get/create_local_encryption_key() to Keychain (write-once)
- Rename encrypted DB files: cove.encrypted.db, wallet_data.encrypted.json.redb
- Rewrite redb migration to source≠dest flow (no two-phase-swap)
- Simplify restore: save master key without replacing local encryption
- Remove replace_encryption_key(), delete_master_key(), Database::reinitialize()
- Fix .expect() panic in wallet_data database_location()
- Wipe BDK store files in wipe_local_data (B2)
- Load persisted cloud backup state on startup via syncPersistedState (B3)
- Add cloud restore option to CatastrophicErrorView with CloudKit probe (B1)
- Add DeviceRestoreView orchestrating wipe → bootstrap → restore (B1)
- Guard against concurrent enable/restore operations (I5)
- Don't mark Enabled when all wallets fail to restore (C2)
- Propagate collect_existing_fingerprints error instead of swallowing (C3)
- Error on partial keychain state in get_local_encryption_key (C4)
- Write-side hardening: cleanup orphaned cryptor on second save failure (C4)
- Bootstrap handles partial keychain state: routes to DatabaseKeyMismatch
  or purges and recreates when no DB exists (C4)
- Zeroize plaintext JSON in wallet_crypto encrypt/decrypt (I1)
- Use .allKeys save policy in CloudKit uploads for idempotent retry (I2)
- Add 120s timeout to passkey semaphore (I3)
- Log encryption key mismatch in release builds (I4)
- Log is_wallet_duplicate errors (I6)
- Distinguish "record exists but data nil" from "not found" in CloudKit (I7)
On startup, if cloud backup state is disabled (fresh install or upgrade),
probe CloudKit for an existing backup. If found, show a simple
CloudRestoreOfferView with restore/skip options before entering the
normal app flow.
Increment Android versionCode from 17 to 18 and update iOS CURRENT_PROJECT_VERSION from 64 to 67 in the Xcode project. These changes align build numbers for a new release while leaving Android versionName (1.3.0) unchanged.
Add request.prf = .checkForSupport during createPasskey so the
authenticator knows PRF will be needed for assertions. Also log
PRF support status after registration and log when assertion.prf
is nil to help diagnose PRF failures.
When authenticateWithPrf fails (e.g. passkey was deleted externally),
clear the cached credential_id and prf_salt from keychain so the next
retry creates a fresh passkey instead of trying to sign in with a
credential that no longer exists.
Runs enable/restore on tokio's blocking thread pool so the tokio
reactor is available if any code inside needs it.
Replace verbose .map_err(|e| Error(e.to_string())) with map_err_str
across cloud_backup_manager, derive strum::EnumIter on WalletMode
for cleaner iteration, add iCloud/CloudKit entitlements, and document
map_err_str preference in CLAUDE.md
Replace verbose .map_err(|e| ...) patterns with cove_util::ResultExt helpers (map_err_prefix / map_err_str) for cleaner, consistent error mapping across modules. Add missing use statements where needed, centralize and move the CloudBackupError enum earlier in cloud_backup_manager.rs (removing the duplicate at the bottom), and make small related fixes (manifest/version handling, whitespace). Also update CLAUDE.md to document the preferred ResultExt helpers. Affected files: CLAUDE.md, rust/crates/cove-device/src/keychain.rs, rust/src/auth.rs, rust/src/backup/crypto.rs, rust/src/backup/verify.rs, rust/src/manager/cloud_backup_manager.rs.
Clear per-wallet DATABASE_CONNECTIONS cache in reinit_database() so
restored wallets don't get stale handles pointing at deleted files.

Add 120s timeout to observeRestoreCompletion() polling loop to prevent
infinite spinner if the Rust side gets stuck.
Rust: Improve legacy DB migration recovery and detection — handle interrupted two-phase swaps (.enc.tmp + .bak), restore from .bak or finish the .enc.tmp rename as appropriate, and avoid re-migrating databases that are already encrypted by the old migration (rename them instead). Added needs_legacy_rename helper, updated counting and migration flows to consider both migration and simple rename cases, and added safety logging and error handling.

iOS: Surface iCloud backup check failures — add cloudCheckError state, present an alert when the cloud backup check throws, and switch the cloud check to a try/catch flow that logs failures and sets the error message while still finishing bootstrap.
Check all networks and modes when deciding whether to offer cloud
restore on startup, preventing incorrect restore offers when wallets
exist on a non-selected network or in decoy mode.

Verify main DB health before deleting legacy .bak files, restoring
from backup if the main DB is corrupt instead of silently dropping
the last recoverable copy.
Auto-backup new wallets as they're created via backup_new_wallet()
called from save_new_wallet_metadata. Add sync button on the cloud
backup detail screen for manually uploading unsynced wallets.

Use the cloud manifest as source of truth: refresh_cloud_backup_detail
downloads the manifest from CloudKit to determine real backup status,
and do_sync_unsynced_wallets compares against the manifest rather than
the local cache.

Extract all_local_wallets() helper to flatten the repeated
network×mode double-loop pattern across the file.
Switch hasCloudBackup() and downloadRecord() from the convenience
db.fetch(withRecordID:) API to CKFetchRecordsOperation to match
how uploads already work. Add mapFetchError helper that logs raw
CKError code, domain, and userInfo for diagnosing container errors.
Handle operation-level failures in fetchRecordsResultBlock.
The local cache (cloud_backup_wallets in global_config) was falsely
reporting wallets as backed up without verifying against the cloud.
Remove the cache entirely and use the cloud manifest to determine
real backup status. Add cloud_only_count to CloudBackupDetail and
fetch_cloud_only_wallets() to download and decrypt orphan entries.
Replace cloud_backup_detail() with is_cloud_backup_enabled() since
the detail screen now loads everything from the cloud.
Show a progress indicator while downloading the cloud manifest instead
of falsely displaying cached backup status. Show error state if the
cloud read fails. Add cloud-only section showing count of wallets in
the cloud but not on this device, with a Get More Info button that
downloads and decrypts their metadata. Add SyncFailed reconcile
message to separate sync errors from global backup state.
When the manifest is not found (e.g. switching from Development to
Production CloudKit), automatically re-upload all local wallets instead
of showing a generic error. For other cloud errors (access denied,
network unavailable), show the specific error message with a retry button.

Extract upload_all_wallets_and_manifest helper shared by enable and
re-upload flows.
CloudKit required manual schema deployment and had bundle ID / container
mismatch issues on TestFlight. iCloud Drive (ubiquity container) eliminates
all of this with zero CloudKit Dashboard setup needed.

- Add ICloudDriveHelper with NSFileCoordinator, NSMetadataQuery wrappers
- Files stored in Data/ (hidden from user) with SHA256-hashed filenames
- Uploads block until confirmed uploaded (waitForUpload polling)
- downloadManifest uses NSMetadataQuery for authoritative NotFound
- hasCloudBackup checks both manifest AND master key existence
- Update entitlements: CloudDocuments + ubiquity container
- Replace CKContainer.accountStatus with ubiquityIdentityToken
- Add sync health indicator to cloud backup detail header
…agement

- Add "Create New Passkey" recovery action on cancelled/failed verification
  for when the original passkey was deleted
- Persist verification state as CloudBackup::Unverified in redb so the
  main settings screen shows "Cloud Backup Unverified" across app restarts
- Remove passkey existence check from startup integrity check to prevent
  brief Face ID flash on launch
- Auto-sync unsynced wallets during deep verify and startup integrity check
- Add retry fallback to backup_new_wallet via full sync on failure
- Deduplicate manifest record_ids to prevent duplicate wallet entries
- Add restore and delete actions for cloud-only wallets with confirmation
- Increase iCloud Drive upload timeout from 10s to 60s
- Refresh detail after cloud wallet restore/delete operations
- Add namespace ID derivation (HKDF-SHA256 from master key) for directory isolation
- Replace flat Data/*.json layout with Data/cspp-namespaces/{namespace_id}/ structure
- Remove BackupManifest, use filesystem as source of truth via NSMetadataQuery
- Fix double-hash bug in wallet filenames (recordId is already SHA256)
- Fix /var vs /private/var symlink mismatch in NSMetadataQuery path comparisons
- Add legacy flat-file detection in hasAnyCloudBackup for old-format discovery
- Simplify DeviceRestoreView to additive restore (no wipe/bootstrap)
- Export NAMESPACES_SUBDIRECTORY constant from Rust for cross-platform consistency
@praveenperera praveenperera force-pushed the cspp-cloud-backup-ios branch 2 times, most recently from c02ed3c to 9fa5e5d Compare March 30, 2026 20:01
Rename the documentation file from CLAUDE.md to AGENTS.md and update the first paragraph to reference AGENTS.md instead of CLAUDE.md. Content remains the same aside from the filename and the internal reference.
Split UI and logic for onboarding startup recovery views for better separation of concerns and testability. CloudCheckView now reports result via a completion closure and its cloud probe logic was extracted into CloudCheckContent and a static checkForCloudBackup helper. CatastrophicErrorView and DeviceRestoreView were refactored to separate Content subviews from task/logic (probing cloud, retry/contact/wipe handlers, restore start/timeout/sync with BackupManager). Added SwiftUI previews for CloudRestoreOffer, CloudCheck, CatastrophicError, and DeviceRestore states.
Add OnboardingRecoveryTypography to centralize font styles and replace many inline .font(.system(...)) calls across onboarding recovery views. Implement a custom restoringHeroIcon in DeviceRestoreView (replacing a previous OnboardingStatusHero usage) and adjust various text sizes, weights, and button styles to use the new typography. Align Terms & Conditions content to leading, tweak padding/spacing and footnote opacity, and refine checkbox card layout for improved consistency and maintainability.
Replace the plain SwiftUI Text for the privacy/terms line with a UIViewRepresentable (TermsAgreementText) backed by a non-editable UITextView to render styled, tappable links. Add @Environment(\.openURL) and a Coordinator to intercept link taps and forward them via openURL. Introduce LinkOnlyTextView with custom hit-testing so only link regions are interactive. Minor callsite update to TermsCheckboxCard to use the new component and remove the allowsCardToggle parameter. This enables properly styled, accessible link interaction inside the terms card.
@praveenperera praveenperera force-pushed the cspp-cloud-backup-ios branch from 9fa5e5d to a5a6714 Compare March 30, 2026 20:03
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.

Define CSPP backup data structures

1 participant