Files
Outpost/OutlineKit
Puranjay Savar Mattas 20baab78c0 feat(outlinekit): encrypt the offline cache at rest
Both CachedPayload and PendingOperation only ever stored their
payload as plain JSON on disk (SwiftData/SQLite, no encryption of its
own) - readable by anyone with access to the logged-in session, per
the earlier discussion on where this cache lives. Adds AES-GCM
encryption at the one place raw bytes cross into/out of
OfflineCacheStore (CachingOutlineAPIClient, which already owns
encode/decode) - OfflineCacheStore itself stays a dumb opaque-blob
store, since its key/id/kind columns can't be encrypted without
breaking the #Predicate queries built against them.

Key management (KeychainCacheEncryptionKeyStore, mirrors
KeychainTokenStore exactly): a random 256-bit key, generated once and
Keychain-stored, not derived from anything guessable. It doesn't need
deriving to survive an uninstall/reinstall either - Keychain items
are scoped to the app's code signature, not its on-disk presence, so
a reinstall of the same app regains access to the same key
automatically (same reason a saved API token already survives a
reinstall today). If the on-disk cache also happens to survive
(dragging the .app to the Trash doesn't clean ~/Library/Containers),
a reinstall can still read it.

A row written before this shipped (still plaintext) or encrypted
under a since-cleared key just fails to decrypt and is treated as a
cache miss - same as any other decode failure, so it silently
refetches and re-caches encrypted rather than crashing. No explicit
migration needed.

Also: OfflineCacheStore.clearEverything() wipes both the cache AND
the pending write queue (clearAll(), used by Settings' "Clear All
Cache", still only touches the cache - it shouldn't silently discard
someone's unsynced edits). Exposed as
CachingOutlineAPIClient.clearEverythingForSignOut(), for sign-out to
use alongside clearing the key.

CacheEncryptionKeyStoring is a protocol (like TokenStoring) so tests
never touch the real Keychain - existing CachingOutlineAPIClientTests
now inject an in-memory StaticCacheEncryptionKeyStore. 8 new tests
(retry/failure-log tests from the previous commit plus 3 new ones
here: ciphertext isn't plaintext JSON, a cleared key makes old rows
unreadable, clearEverythingForSignOut wipes both tables). 95/95
passing.
2026-08-21 01:15:15 +01:00
..