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.
17 lines
746 B
Swift
17 lines
746 B
Swift
import Foundation
|
|
import CryptoKit
|
|
|
|
/// Boundary over wherever the offline cache's symmetric encryption key
|
|
/// lives. Mirrors `TokenStoring` — same reasoning, different secret.
|
|
public protocol CacheEncryptionKeyStoring: Sendable {
|
|
/// Returns the existing key, generating and persisting a new random one
|
|
/// on first use if none exists yet.
|
|
func key() throws -> SymmetricKey
|
|
/// Called on sign-out. Anything still encrypted with the cleared key
|
|
/// becomes permanently unreadable — that's the point, not a bug: the
|
|
/// next person signed in on this machine shouldn't be able to read a
|
|
/// previous account's cached content just because the on-disk rows
|
|
/// happen to still be there.
|
|
func clear() throws
|
|
}
|