Conversation
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…s→clipShape project-wide
…() modifier Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
SecureGalleryView called both onDismiss?() and the environment dismiss() at every dismissal site. When the gallery is a pushed nav destination (Camera -> Gallery -> Select for Decoys), onDismiss is nav.navigateBack(), so Save popped the stack twice -- removing .gallery then .camera -- landing on the empty Color.clear root (black screen). Call exactly one mechanism: the injected onDismiss when present, otherwise the environment dismiss(). Fixes empty-gallery, decoy Back, and decoy Save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Videos are stored in a separate "videos" directory, but activatePoisonPill() only wiped the photo gallery and thumbnails via deleteNonDecoyImages(). All videos therefore survived the poison pill -- a serious data-leak that defeats the feature's purpose. Add deleteNonDecoyVideos(), invoked before deleteNonDecoyImages() (which removes the decoy directory used for the decoy check). A video is preserved only if a file with the same name exists in the decoy directory; since decoy selection is photo-only today, every video is destroyed -- while remaining forward-compatible if video decoys are added later. Tests: PoisonPillVideoDeletionTests covers both the destroy and decoy-preserve paths. To make them run, the previously orphaned FakeEncryptionScheme / FakeThumbnailCache test helpers were added to the SnapSafeTests target (and FakeEncryptionScheme updated to match the current EncryptionScheme protocol). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A number of test files existed on disk but were never members of the SnapSafeTests target, so they silently never compiled or ran. Removed (obsolete/superseded - tested types that no longer exist): - SecureFileManagerTests, EditedPhotoTrackingTests (SecureFileManager removed) - LocationManagerTests (replaced by LocationRepository) - PINManagerTests (replaced by PinRepository/PinCrypto) - SecurePhotoTests (SecurePhoto model removed) - CameraModelTests (CameraModel renamed to CameraViewModel) - CameraLifecycleTests (CameraViewModel.isSessionActive removed) - FaceDetectorTests (MaskMode reduced to .pixelate; blurFaces removed) - PhotoDetailViewModelTests (built on removed SecurePhoto + showFaceDetection) - SnapSafeTests (empty Xcode template stub) Folded into the target (valid tests of current code, with minor fixes): - VerifyPinUseCaseTests - updated to current AuthorizePinUseCase / VerifyPinUseCase initializers; import FactoryKit; @mainactor. - SECVFileFormatTests - UInt32 conversions; fileLength: label. - SecureImageRepositoryTests - drop removed getPhotoByName tests; saveImage now takes CLLocation; add getVideosDirectory override for isolation. - Added the previously-orphaned FakeEncryptionScheme / FakeThumbnailCache helpers to the target. Full unit suite now compiles and runs: 92 passed, 0 failed (was 58). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the poison pill destroyed ALL videos because videos could never be marked as decoys: saveDecoySelections() skipped video items and there was no video-decoy storage. Even preserving a video as-is would be broken, since activatePoisonPill deletes the real key's DEK, making real-key content undecryptable. Mirror the photo decoy model for videos: - VideoEncryptionService: add encryptVideoForDecoy(...) async (awaitable encrypt). - SecureImageRepository: inject VideoEncryptionService; add isDecoyVideo, addDecoyVideoWithKey (decrypt with current key -> re-encrypt with poison-pill key into the decoy dir), removeDecoyVideo; count videos toward the shared decoy limit. deleteNonDecoyVideos now destroys non-decoy videos AND moves each decoy's poison-pill-key copy into the videos dir, replacing the real-key original (runs before deleteNonDecoyImages, which removes the decoy dir). - AddDecoyVideoUseCase + DI wiring (and pass VideoEncryptionService into the SecureImageRepository factory). - MixedMediaGalleryViewModel: saveDecoySelections add/remove decoy videos; decoy count, pre-selection, and strings now include videos. Result: decoy videos are re-encrypted with the poison-pill key and remain playable under the duress PIN; all non-decoy videos are destroyed. Tests (PoisonPillVideoDeletionTests, + FakeVideoEncryptionService): - non-decoy videos destroyed (decoy photo preserved) - addDecoyVideoWithKey re-encrypts and marks the video a decoy - end-to-end: mark decoy video -> poison pill -> decoy file replaced by the poison-pill-key copy, non-decoy video destroyed Full unit suite: 93 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marking a video as a decoy re-encrypts it with the poison-pill key, which can
take a while for large videos. Previously the decoy gallery dismissed
immediately and the work ran detached, giving the user no feedback.
- saveDecoySelections() is now async and drives isSavingDecoys + a
completed/total counter (only items whose decoy state changes are processed).
- SecureGalleryView awaits the save and dismisses only when it completes,
showing a dimmed spinner overlay ("Saving decoy media…" with an N-of-M count
for multiple items). Save/Back are disabled while saving.
Note: the SECV re-encryption still runs on the main actor (the
VideoEncryptionService is @mainactor). The indeterminate ProgressView animates
on the render server so the spinner stays alive, but the rest of the UI is
blocked during the crypto. Moving the crypto off the main actor is a follow-up;
the recording path already consumes its progress via receive(on: .main), so it
should be feasible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Video cells showed a generic film-icon placeholder. Now each video has a real thumbnail. Because videos are encrypted SECV (unreadable by AVAssetImageGenerator), the thumbnail is generated once at record time from the plaintext .mov, before it is deleted. - SecureImageRepository: durable, encrypted video-thumbnail storage in Application Support (videoThumbnails/, excluded from backup) — generate from the plaintext .mov via AVAssetImageGenerator.image(at:), store encrypted with the current key, read+decrypt with an in-memory cache, delete one / delete all. - Security: thumbnails are derived from real frames, so deleteAllVideoThumbnails runs on poison-pill activation and security reset; per-video thumbnail (and any decoy copy) is removed when a video is deleted. - ThumbnailCache: video-name-keyed get/put/evict (prefixed). - CameraViewModel.encryptRecordedVideo generates+stores the thumbnail before the .mov is deleted. - VideoCellView loads the decrypted thumbnail via .task (mirrors PhotoCell), with a play badge and decoy shield; falls back to the icon placeholder. Scope: record-time only — videos recorded before this change and decoy videos (after the pill) show the placeholder. Tests (VideoThumbnailTests): store writes an encrypted file; read returns the image; delete removes it; poison pill wipes the whole thumbnails dir. Full unit suite: 97 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SecureGalleryViewModel was unreferenced (the live gallery uses MixedMediaGalleryViewModel). Removed the 22KB class and file. The file also declared the shared SelectionMode enum, which the live view model uses, so that enum was moved into MixedMediaGalleryViewModel.swift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
~13 test files had silently never been added to the SnapSafeTests target, so they never compiled and their tests never ran (the bundle reported success while executing nothing). Add scripts/check_test_target_membership.rb, which fails if any .swift under SnapSafeTests/ is not compiled by the target, and run it first in the fastlane build/test lanes (so CI's `fastlane test` and local runs both enforce it). Verified it fails on an orphan file and passes when clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rship guard Add a "Building, Testing & Releasing" section covering the fastlane lanes, the test-target membership guard (scripts/check_test_target_membership.rb) and how it's enforced, the CI workflows, and the tag-driven release process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l deleted ALL videos addDecoyVideoWithKey decrypted the original to a temp file and re-encrypted it with the poison-pill key, but never created those output files. The video encryption service opens its output with FileHandle(forWritingTo:), which requires the file to already exist (the camera and the sharing path both pre-create it). So every decrypt/encrypt threw, addDecoyVideoWithKey caught it and returned false, and NO video was ever marked as a decoy. Consequences, both reported by the user: - Entering the poison PIN destroyed ALL videos, including ones the user tried to mark as decoys (deleteNonDecoyVideos found zero decoy files). - The decoy shield badge never appeared on video cells (isDecoyVideo was always false). Fix: pre-create the temp plaintext file and the decoy file before calling the encryption service, matching the camera/sharing pattern. Why tests missed it: FakeVideoEncryptionService wrote output via Data.write(to:), which creates the file, masking the missing precondition. The fake now models the real precondition (throws if the output file does not exist), so this class of bug is caught. With the faithful fake the two decoy tests went RED, then GREEN after the fix. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…haring + decoy marking) decryptVideoFile read `upToCount: trailer.chunkSize` (the full 1 MB) for EVERY chunk, but the encoder writes a partial final chunk (min(chunkSize, remaining)). On the last chunk that over-read swallowed the auth tag (and index/trailer), so the subsequent tag read returned nothing and decryption threw SECVError.fileIOError -- for essentially any video (almost all have a partial final chunk). This silently broke every bulk-decrypt caller, decryptVideoForSharing: - Marking a video as a decoy (addDecoyVideoWithKey decrypts then re-encrypts) always failed, so no video ever became a decoy. Hence the poison pill deleted ALL videos and the decoy shield badge never appeared. - Video sharing (also decryptVideoForSharing) was broken the same way. Playback was unaffected -- it streams via a custom AVAssetResourceLoader (makeEncryptedVideoAsset), a different path. Fix: read each chunk's actual size, min(chunkSize, originalSize - chunkIndex*chunkSize); AES-GCM ciphertext length equals the plaintext length, so this reads exactly the ciphertext and leaves the tag intact. This was the real root cause behind the decoy-video badge / "all videos deleted" reports (the earlier saveDecoySelections and pre-create fixes were necessary but not sufficient). Tests (DecoyVideoIntegrationTests): real-service encrypt/decrypt round-trips for single-chunk and multi-chunk-with-partial-last inputs (assert exact bytes), plus an end-to-end "mark video as decoy -> isDecoyVideo true" using the real VideoEncryptionService. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On poison-pill activation, deleteAllVideoThumbnails() destroys every video thumbnail (they are derived from real frames and were encrypted with the now-deleted real key). But decoy videos survive the pill, so they were left with no thumbnail -> the gallery showed the placeholder icon. Mirror the decoy-video mechanism for thumbnails: when a video is marked as a decoy, re-encrypt its thumbnail with the poison-pill key into a separate decoyVideoThumbnails/ directory. On the pill, after wiping the real-key thumbnails, restore the decoy thumbnails into videoThumbnails/. They are encrypted with the poison key (the active key after the pill), so readVideoThumbnail decrypts them normally; clearAllThumbnails() flushes the in-memory cache so no real thumbnail leaks. Cleanup: removeDecoyVideo drops the decoy thumbnail copy; securityFailureReset wipes the decoy thumbnail directory too. Test (VideoThumbnailTests): marking a video as a decoy stores a poison-key thumbnail; after the poison pill the decoy video's thumbnail is restored while a non-decoy video's thumbnail is destroyed. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Import picker only accepted images. Allow videos too: a picked video is copied to a temp file (ImportedMovie transferable), encrypted to SECV with the current key in the videos directory, and given a thumbnail -- mirroring the camera record path. Imported videos use the camera's "video_yyyyMMdd_HHmmss" naming (bumping the second on collision) so they stay unique and sort correctly. Tests (VideoImportTests + DecoyVideoIntegrationTests): import creates an encrypted .secv with the video_ prefix, the name is date-parseable/sortable, repeated imports get distinct names, a non-decodable input still encrypts but skips the thumbnail, and a real-service round-trip recovers the original bytes. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the flat black translucent backgrounds on the camera controls (switch/flash/gallery/settings buttons, recording indicator, zoom capsule) with a glassControlBackground modifier: Apple Liquid Glass (glassEffect) on iOS 26+, with an .ultraThinMaterial fallback for the iOS 18.5 deployment floor. The shutter keeps its dedicated design. The glass is intentionally non-interactive: these backgrounds sit inside Buttons and tap gestures, and interactive glass installs its own touch handling that swallowed the button taps (regression: the flash toggle could not be changed). The enclosing control owns the interaction; the modifier is purely visual. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping any gallery item opened photos in a swipe-only sequence; videos went
to a separate full-screen player with no surrounding context and no way to
swipe back to adjacent media. The detail route carried [PhotoDef] so videos
were stripped before the pager started.
Fix: AppDestination.photoDetail now carries [GalleryMediaItem] (the full
mixed-media list). PhotoPageViewController creates a PhotoDetailHostingController
for photos or an InlineVideoHostingController for videos, using the encryption
key already on each GalleryMediaItem. All gallery taps route to .photoDetail
with viewModel.mediaItems so swiping moves seamlessly between photos and videos
in gallery order.
InlineVideoPageView wraps VideoPlayerViewModel + AVKit VideoPlayer. Requires
.onAppear { viewModel.setupPlayback() } — without it isLoading stays true
forever and the spinner never resolves.
EnhancedPhotoDetailViewModel works with [GalleryMediaItem]: currentPhotoDef /
currentIsVideo gate photo-specific operations; preloading skips video items.
Photo toolbar hides when the current page is a video (AVKit provides controls).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntrols
- New InlineVideoPlayerView replaces AVKit's chrome with a bare
AVPlayerLayer surface (VideoSurfaceView), a glass transport bar
(play/pause, scrubber, time), and the shared MediaDetailToolbar
(Share / Decoy / Delete). Photo and video pages now use one
toolbar component.
- Auto-hide rewritten: replaced the global Timer.publish with a
per-show cancellable Task that resets on every interaction
(play/pause, scrub start/end, tap-to-show) and waits 5s before
fading. Scrubbing cancels the timer so the bar can't disappear
mid-drag.
- Play/pause hit area: added contentShape(Rectangle()) so the full
44x44 frame is reliably tappable instead of falling through to
the surface tap that toggles control visibility.
- Counter chip ("3 of 10") now fades in lockstep with the video
controls via a callback plumbed through PhotoPageViewController
to EnhancedPhotoDetailViewModel; photo pages remain unaffected.
- Off-screen audio fix: the asset load now runs in a tracked Task
that cleanup() cancels, and the completion bails out before
player.play() when the task is cancelled -- so a slow decrypt
can't auto-start audio on a page that's been swiped away.
- Haptics: light sensory feedback on play/pause (keyed to
isPlaying) and on every MediaToolbarButton tap, matching the
camera/PIN vocabulary already in the app. The standard SwiftUI
Slider continues to provide scrub haptics for free.
- Project, strings catalog, and fastlane README updated for the
new files and lanes. TODO.md adds a scratch list of unrelated
observations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| ErrorProne | 1 critical |
🟢 Metrics 589 complexity · 34 duplication
Metric Results Complexity 589 Duplication 34
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
DEK files were written without `.completeFileProtection`, making them readable from first device unlock until reboot—even while locked. Wrapped DEKs are now encrypted at rest with the `.complete` file-protection class: - Line 286: DEK file writes use `.completeFileProtection` + `.atomic` options so files are atomic and unreadable when device is locked - Line 537: Keys directory marked with FileProtectionType.complete to protect all contained key material Fixes H1: DEK files written without `.completeFileProtection`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`VerifyPinUseCase` used `try!` around `deriveAndCacheKey`, so any I/O error reading the wrapped DEK, transient hardware-key failure (e.g. `errSecInteractionNotAllowed` when the device locks mid-flow), or PBKDF2 failure crashed the process. An attacker who can race the device-lock state could turn that into a DoS. - Introduce `PinVerificationResult` (`success` / `invalidPin` / `failure(Error)`) and return it from `verifyPin` instead of `Bool`. - Catch derivation errors and surface them as `.failure`. - Update `PINVerificationViewModel` to treat `.failure` as a retryable error that does NOT increment failed-attempts (otherwise the same race could force a security reset). - Show the new retryable-error message in `PINVerificationView`. Test (red→green): `test_verifyPin_returnsRetryableFailure_whenKeyDerivationThrows` stubs `deriveAndCacheKey` to throw and asserts the use case returns `.failure` without crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was needed to address the new xcode26 compiler issues we didn't see on the earlier version.
`securityFailureReset` left the Secure Enclave EC keys (`snapsafe_kek`, `pin_key`) intact in the keychain, so they survived reset and re-onboarding and could decrypt any DEK that ever leaked via backup or extraction. `evictKey()` also launched a detached `Task` and returned synchronously, so the in-memory key could still be cached during/after the reset. - `securityFailureReset` now `SecItemDelete`s every EC hardware key the app owns (`kSecClassKey` + `kSecAttrKeyTypeECSECPrimeRandom`) and awaits `evictKey()` before returning. - `EncryptionScheme.evictKey()` and its implementations are now `async`. Ripples to `SecureImageRepository.evictKey/securityFailureReset/ activatePoisonPill` and `InvalidateSessionUseCase.invalidateSession` (now `async`); call sites updated to `await`. Tests (red-first, in `HardwareEncryptionSchemeSecurityResetTests`): - `test_securityFailureReset_deletesHardwareKeys` creates the KEK and `pin_key` via `encryptWithKeyAlias`, asserts they exist in the keychain, runs reset, and asserts both are gone. - `test_securityFailureReset_evictsCachedKeyBeforeReturning` derives a key, runs reset, and asserts `getDerivedKey()` throws `.keyNotDerived` on return — proving the cache was cleared before reset returned. Both tests failed before this change and pass after. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wall-clock elapsed-time checks let an attacker bypass the session timeout (move clock backward) or zero out the PIN backoff (move clock forward). Switch elapsed-time decisions to CLOCK_UPTIME_RAW via a new Clock.monotonicNow accessor; keep wall clock only for display and restart-fallback persistence with backward deltas clamped to 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
verifyPoisonPillPin was awaited on every PIN attempt even when no poison pill is configured. That ran a second Argon2 verification per attempt and gave an attacker a timing oracle for poison-pill presence. Gate the call behind hasPoisonPillPin via && short-circuit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UserDefaultsSettingsDataSource carried a hardcoded "stub-cipher-key" constant that was seeded into UserDefaults and persisted in the file-based settings JSON. Audit confirmed no consumer ever reads getCipherKey() — the protocol method, both implementations, the Defaults constant, the PrefKeys case, and the SettingsData field were dead. Removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hardware-backed DEK could be unwrapped without the SnapSafe PIN once the
device was unlocked. The DEK was derived from the PIN at create time
(PBKDF2(PIN ‖ dSalt ‖ deviceID)) but only Secure-Enclave-wrapped on disk, so
the load path reproduced the DEK from the SE alone — `deriveWrappedKey` never
used its `plainPin` argument. The Argon2 PIN check was a Swift-level gate, not
a cryptographic dependency. Anything reaching `SecKeyCreateDecryptedData` on an
unlocked device (jailbreak, lldb attach, patched binary) recovered all content
without the PIN.
Fix: add a PIN-derived AES-GCM wrap layer *under* the SE wrap.
create: DEK = random(32) // independent of PIN
pinKey = PBKDF2("snapsafe-pinwrap-v1:" ‖ PIN ‖ deviceID, salt)
stored = SE_wrap( AES-GCM(DEK, key: pinKey) )
derive: payload = SE_unwrap(stored)
DEK = AES-GCM-open(payload, key: pinKey) // wrong PIN -> .wrongPin
Recovering the DEK now requires the user to actively type the PIN. Chosen over
`.userPresence` / biometric ACLs deliberately: biometrics and the device
passcode are coercible (sleeping/forced face, border demand), the PIN is not —
SnapSafe's threat model is compelled-access resistance. See
design/2026-05-31-c1-pin-binding-analysis.
Details:
- New `PinDEKWrapper` (CryptoKit + PBKDF2, keychain-free → unit-testable on CI):
derivePinKey / wrap / unwrap / isLegacyRawDEK.
- `CryptoError.wrongPin` (+ Equatable) for clean, non-leaky wrong-PIN failures.
- One-shot transparent migration: a legacy 32-byte raw-DEK payload is preserved
(existing content depends on its value) and re-wrapped under the PIN key on
next valid unlock. 32-byte raw vs 60-byte wrapped is the discriminator.
- Removed now-dead PIN→DEK PBKDF2 path (`derivePBKDF2Key`, `dSaltSize`).
- UX unchanged: same `deriveAndCacheKey` signature, same session model.
Tests (TDD): 10 PinDEKWrapper unit tests (determinism, wrong-PIN rejection,
no-plaintext-leak, nonce freshness, payload length, migration discriminator)
run on CI; 3 scheme-level integration tests (round trip, wrong-PIN, stored
payload is PIN-wrapped) run on device, skip on simulator (Secure Enclave).
Full suite: TEST SUCCEEDED, 0 failures.
Addresses C1 from the 2026-05-31 security review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The failed-attempt counter had two writers. On an invalid PIN, VerifyPinUseCase called incrementFailedAttempts() — which authoritatively bumps the persisted count AND records the last-failed timestamp + monotonic backoff baseline. Then PINVerificationViewModel ALSO wrote the counter via setCurrentFailedAttempts(failedAttempts + 1), derived from its stale local @published value and without touching the backoff timestamp/baseline. The two writes happened to align today, but the design was racy (the VM's local value updates asynchronously in a Task) and could desync the count from the backoff window — a value the lockout/security-reset logic depends on. Fix: make the repository the single writer; the view model only observes. - PinVerificationResult.invalidPin now carries `failedAttempts: Int`, the authoritative post-increment count returned by the repository's single increment. - VerifyPinUseCase returns that count; the view model reflects it into its @published property and uses it for the MAX-attempts check — it no longer writes the counter. - On success the VM just sets failedAttempts = 0 locally (AuthorizePinUseCase already reset the persisted counter); the redundant write is gone. - Removed the now-dead setCurrentFailedAttempts(_:) helper. Tests (TDD): added test_verifyPin_onInvalidPin_incrementsCounterExactlyOnce_ andReturnsNewCount — asserts the persisted counter goes 0 -> 1 on one invalid attempt and the result carries count 1. Full suite: TEST SUCCEEDED, 0 failures. Addresses M1 from the 2026-05-31 security review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tial) The settings file (AppSettings.json) persists the reversible poison-pill PIN ciphertext (poisonPillPlain) alongside the PIN hashes, and was written with `.atomic` only — excluded from backup but readable while the device is locked. Apply `.completeFileProtection` to the write so the file is unreadable when the device is locked (defense-in-depth at rest). This is Option D from the H2 risk discussion: a hardening pass, not a full remediation. The reversible PP PIN still exists because decoy creation needs to reproduce the poison-pill encryption key while the user is in their normal session (AddDecoyPhoto/VideoUseCase). The deeper fix (wrap the poison-pill DEK under the primary PIN key, à la C1, and drop the reversible PIN) is deferred — the residual exposure (recover the PP PIN value via instrumented access on an unlocked device) was assessed as mostly theoretical. See decisions/2026-06-01-poison-pill-pin-accepted-risk. Test (device-only; skips on simulator where file protection isn't enforced): asserts the settings file has `.complete` protection after a PP PIN is stored. Full suite: TEST SUCCEEDED, 0 failures. Addresses H2 (partial / accepted residual risk) from the 2026-05-31 security review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is just about done, checking it out here.