Skip to content

Snapshot the rebuilt backup and encryption-edit screens - #573

Merged
bradleymackey merged 1 commit into
mainfrom
test/rebuilt-screen-snapshots
Sep 15, 2026
Merged

bradleymackey merged 1 commit into
mainfrom
test/rebuilt-screen-snapshots

Conversation

@bradleymackey

Copy link
Copy Markdown
Member

Changes (test-only) — final PR of the pre-release audit series (#561–#572)

Snapshot coverage for security-relevant screens rebuilt in #551–#560 that had no view tests:

  • BackupKeyDecryptorViewSnapshotTests — initial state (light/dark × 3 type sizes, in a NavigationStack so the Cancel toolbar renders) plus a deterministic decrypt-failure state: the failure is produced by actually running attemptDecryption() with the fast testing deriver and an erroring decoder mock, not by faking view state.
  • BackupImportFlowViewSnapshotTests — all three BackupImportContext variants (empty vault / merge / override), light/dark.
  • VaultDetailEncryptionEditViewSnapshotTests — encryption-disabled (full grid) and encryption-enabled variants.
  • SettingsDangerView was covered in Cover the Danger Zone view model and add its first snapshots #565; AutoBackupSettingsView is deliberately not given a standalone suite — it is already snapshotted transitively through BackupCreateView (BackupViewSnapshotTests), and its enabled/error states are only reachable through an async .task handoff that would flake under synchronous snapshot rendering. Driving those states needs a small initial-state injection refactor — left as follow-up.

Release gate: full CI_iOS scheme (all 13 test targets, iOSAllTests plan including the TSAN configuration) run locally on iPhone 18 Pro Max / iOS 27.0.

⚠️ Automatic CI is still disabled (#548), so this is local verification only.


Release findings — report-only (no code in this series)

The pre-release audit surfaced the following items that need design decisions, not patches. Recorded here so they are not lost:

MANIFESTO C7 gaps (protective defaults):

  • Clipboard paste TTL defaults to never-expire (PasteTTL.default = nil) — copied OTPs/passwords sit on the pasteboard indefinitely unless the user opts in to a TTL.
  • No screenshot / app-switcher privacy protection anywhere (no privacySensitive(), no capture detection, no cover view).
  • Danger Zone full wipe has no confirmation dialog — one tap + biometric.

Design-level:

  • Backups export killphrase/search-passphrase salts+digests; anyone holding the backup password can enumerate which items are duress-protected (C5 tension).
  • The killphrase/search-passphrase HMAC keys are device-local and not exported, so a restore onto a new device silently disarms every killphrase and permanently hides .onlyPassphrase items (rows exist, digests unverifiable).
  • No app-level lock / auto-lock; background purge clears only the backup password from memory.
  • Killphrase-triggered auto-backup + widget reload is an out-of-band success signal for a hidden item's deletion (C2 tension).
  • payloadHash and lastBackupHash live in plaintext UserDefaults — mutation-time evidence (C6 tension).
  • deleteVault() does not refresh the auto-backup hash, so the newest auto-backup still describes the wiped vault (recovery safety net vs C6 — decide).
  • DerivedEncryptionKey.debugDescription prints raw key material as hex; keychain replace (remove→store) is non-atomic; killphrase/passphrase edit fields are plain TextField not SecureField; the vault:// HOTP-increment deep link is unauthenticated; Data.random relies on SystemRandomNumberGenerator (CSPRNG on Apple platforms, but unannotated as the app's sole randomness source); no protectedDataWillBecomeUnavailable handling.

Hygiene (non-blocking): CI triggers commented out; CHANGELOG ~9 versions stale vs MARKETING_VERSION 2.0; hardcoded strings in rebuilt screens bypass the string catalogs (app is currently English-only, so cosmetic); stale scheme/test-plan references (CI_iOS scheme, orphan VaultUITests scheme) and a stale snapshot directory; VaultBackup.xcstrings not declared as a target resource; the keygen speedtest CLI prints a derived key in hex; feed search reload has no debounce/cancellation; ForEach identity built from Hasher().finalize(); reorder persist failures are swallowed.

🤖 Generated with Claude Code

BackupKeyDecryptorView (including a real driven decrypt-failure
state), BackupImportFlowView in all three import contexts, and
VaultDetailEncryptionEditView had no view coverage since the SwiftUI
rebuild. AutoBackupSettingsView stays covered transitively through
BackupCreateView; its async-driven states need an initial-state
injection refactor to snapshot without flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bradleymackey
bradleymackey merged commit 2217314 into main Sep 15, 2026
@bradleymackey
bradleymackey deleted the test/rebuilt-screen-snapshots branch September 15, 2026 14:52
bradleymackey added a commit that referenced this pull request Sep 15, 2026
## Summary

The backup surface was split across two sidebar peers ("Backup" and
"Restore"), with the Backup screen stacking four unrelated jobs —
password management, inline auto-backup settings, PDF export, and device
transfer — into one form. The password screen's "Generate Key" action
also read as password generation when it actually derives an encryption
key from a typed password. This PR restructures the IA:

- **Single "Backups" sidebar item** opening a hub: a last-backup status
banner (revives the orphaned `LastBackupSummaryView`) plus rows for
Auto-Backup, Export, Restore, and Backup Password. `BackupCreateView`
becomes `BackupExportView` (PDF + device transfer only); its
strings-only view model and nine orphaned xcstrings keys are deleted.
- **Backup password screen reframed**: the action is "Set Backup
Password" (revealed alongside the entry fields once a password is
typed), key derivation is presented as a progress state with distinct
cancelled/error copy, and the About text explains derivation without the
"generate" framing. Keygen cancellation and plaintext clearing are
untouched.
- **Auto-Backup gets its own screen** backed by a new
`AutoBackupViewModel` (the initial-state-injection refactor deferred in
#573): state is seeded synchronously from the service, provider states
are injectable for tests, the destination row shows provider name +
folder with a "Change" affordance, folder-configure failures surface as
an error row with a recovery suggestion (previously swallowed by an
empty `catch`), and the active provider resolves from configuration
instead of a hardcoded `availableProviders.first`.

## Manifesto review

Reviewed against `MANIFESTO.md`, corollary by corollary, since backups
are explicitly governed by it. This is a navigation and copy
restructure: backup payloads, encryption, and every action's mechanics
are untouched. **C1** — no bulk operation added; nothing touches
per-item safety fields. **C2/C3** — no new logging, telemetry, or error
channels near duress features; the one new error surface (auto-backup
folder configuration failures, previously swallowed by an empty `catch`)
reports storage-provider errors only. **C4** — device authentication
still gates every operation that loads or uses the backup key: export,
password set/change, the auto-backup screen, and restore imports all
authenticate exactly as before; the new un-gated hub adds navigation,
not capability. **C5** — the hub banner shows backup recency and kind,
never contents; no enumeration of protected items. **C6** — no undo, no
audit surface. **C7** — no protective default flipped; auto-backup
remains opt-in and its configuration remains behind device auth. **C8**
— no sensitive operation lost a step: merging the sidebar items removes
a hop between two navigation screens, while every export, restore, and
password flow retains its full sequence including authentication; the
password screen reframe changes verbs ("Generate Key" → "Set Backup
Password"), not gates. **C9** — backup surfaces never referenced duress
features; unchanged. **C10** — backup, export, and transfer payloads are
byte-identical. One visibility change flagged deliberately: the
last-backup banner (date + kind) is now visible without device auth,
where the old Backup screen showed nothing pre-auth. Weighed against
C4/C7: the same fact is already discoverable without auth (the Restore
screen is un-gated today; auto-backup files are visible in the Files
app), the banner reveals neither destination, contents, nor
protected-item structure, and surfacing backup staleness is itself a
data-loss protection.

## Testing

- Full `iOSAllTests` plan passes locally on iPhone 18 Pro Max / iOS
27.0.
- New suites: `AutoBackupViewModelTests` (19 tests),
`AutoBackupViewSnapshotTests` (7 scenarios — the coverage #573
deferred), `LastBackupSummaryViewSnapshotTests` (4, with an injectable
`now` for deterministic staleness).
- Re-recorded and visually reviewed: `BackupKeyChangeViewSnapshotTests`,
`BackupViewSnapshotTests` (hub + export),
`VaultMainNavigationViewSnapshotTests`.
- Simulator walk-through of the full flow passed: single sidebar entry,
hub layout, every push/back/sheet, and every auth gate failing closed.
- Not automatable (per #556): a real folder pick through `.fileImporter`
— needs one manual run with iCloud Drive.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <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