From 09d9130420fda20b64f7675661412637c30f3d1b Mon Sep 17 00:00:00 2001 From: Bradley Mackey Date: Tue, 15 Sep 2026 10:13:37 +0400 Subject: [PATCH] Prove passphrase-hidden items fail closed without a matcher Nothing asserted that onlyPassphrase items stay hidden when the search-passphrase matcher is nil, which is the real state after a keychain key-load failure. Cover the nil-matcher and wrong-phrase paths with hidden items whose titles would otherwise match the text query. Co-Authored-By: Claude Fable 5 --- .../PersistedLocalVaultStoreTests.swift | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Vault/Tests/VaultFeedTests/Storage/PersistedLocalVaultStoreTests.swift b/Vault/Tests/VaultFeedTests/Storage/PersistedLocalVaultStoreTests.swift index 5de938149..6fc03be01 100644 --- a/Vault/Tests/VaultFeedTests/Storage/PersistedLocalVaultStoreTests.swift +++ b/Vault/Tests/VaultFeedTests/Storage/PersistedLocalVaultStoreTests.swift @@ -697,6 +697,50 @@ final class PersistedLocalVaultStoreTests { #expect(result.errors == []) } + @Test + func retrieveMatchingQuery_keepsOnlyPassphraseItemsHiddenWhenMatcherNil() async throws { + let digester = SearchPassphraseDigester(key: .zero()) + // The hidden item's title matches the text query, so the text + // predicate alone would leak it if searchableLevel were + // mishandled. This is the state after a keychain key-load failure + // leaves the matcher nil — hidden items must fail closed. + let hiddenID = try await sut.insert(item: anySecureNote(title: "aaa").wrapInAnyVaultItem( + searchableLevel: .onlyPassphrase, + searchPassphrase: digester.makeDigest(phrase: "aaa"), + ).makeWritable()) + let controlID = try await sut.insert( + item: anySecureNote(title: "aaa").wrapInAnyVaultItem(searchableLevel: .full).makeWritable(), + ) + + let query = VaultStoreQuery(filterText: "aaa") + let explicitNil = try await sut.retrieve(query: query, searchPassphraseMatcher: nil) + let convenience = try await sut.retrieve(query: query) + + #expect(explicitNil.items.map(\.metadata.id) == [controlID]) + #expect(convenience.items.map(\.metadata.id) == [controlID]) + #expect(explicitNil.items.map(\.metadata.id).contains(hiddenID) == false) + #expect(explicitNil.errors == []) + } + + @Test + func retrieveMatchingQuery_keepsOnlyPassphraseItemsHiddenForWrongPhrase() async throws { + let digester = SearchPassphraseDigester(key: .zero()) + let hiddenID = try await sut.insert(item: anySecureNote(title: "bbb").wrapInAnyVaultItem( + searchableLevel: .onlyPassphrase, + searchPassphrase: digester.makeDigest(phrase: "secret phrase"), + ).makeWritable()) + let controlID = try await sut.insert( + item: anySecureNote(title: "bbb").wrapInAnyVaultItem(searchableLevel: .full).makeWritable(), + ) + + let query = VaultStoreQuery(filterText: "bbb") + let result = try await sut.retrieve(query: query, searchPassphraseMatcher: digester) + + #expect(result.items.map(\.metadata.id) == [controlID]) + #expect(result.items.map(\.metadata.id).contains(hiddenID) == false) + #expect(result.errors == []) + } + @Test func retrieveMatchingQuery_returnsCorruptedItemsAsErrors() async throws { let codes: [VaultItem.Write] = [