import Foundation import Testing @testable import SecureStorage private final class MockKeychainClient: KeychainClient, @unchecked Sendable { enum Failure: Error, Equatable { case locked, unexpected } var values: [String: Data] = [:] var failure: Failure? var adds = 0 var updates = 0 var deletes = 0 func add(service: String, account: String, data: Data, accessibility: KeychainAccessibility) throws { if let failure { throw failure == .locked ? KeychainError.locked : KeychainError.unexpectedStatus(-1) } guard values[account] == nil else { throw KeychainError.unexpectedStatus(-25299) } values[account] = data; adds += 1 } func read(service: String, account: String) throws -> Data { if let failure { throw failure == .locked ? KeychainError.locked : KeychainError.unexpectedStatus(-1) } guard let value = values[account] else { throw KeychainError.notFound } return value } func update(service: String, account: String, data: Data) throws { if let failure { throw failure == .locked ? KeychainError.locked : KeychainError.unexpectedStatus(-1) } guard values[account] != nil else { throw KeychainError.notFound } values[account] = data; updates += 1 } func delete(service: String, account: String) throws { if let failure { throw failure == .locked ? KeychainError.locked : KeychainError.unexpectedStatus(-1) } guard values.removeValue(forKey: account) != nil else { throw KeychainError.notFound } deletes += 1 } } @Suite("Keychain TV authorization store") struct KeychainTVAuthorizationStoreTests { @Test func addReadUpdateDeleteAndNotFound() async throws { let client = MockKeychainClient() let store = KeychainTVAuthorizationStore(client: client, accessibility: .whenUnlockedThisDeviceOnly) let tv = UUID() #expect(try await store.securityRecord(for: tv) == nil) try await store.saveToken("token-1", for: tv) #expect((try await store.authorization(for: tv)).token == "token-1") try await store.saveToken("token-2", for: tv) #expect((try await store.authorization(for: tv)).token == "token-2") try await store.forgetTelevision(tv) #expect(try await store.securityRecord(for: tv) == nil) try await store.forgetTelevision(tv) } @Test func certificateApprovalPinsAndChangedCertificateRequiresReapproval() async throws { let client = MockKeychainClient() let store = KeychainTVAuthorizationStore(client: client) let tv = UUID() let first = Data(repeating: 0x01, count: 32) let changed = Data(repeating: 0x02, count: 32) #expect(try await store.validateCertificate(sha256: first, for: tv) == .notPinned) try await store.approveCertificate(sha256: first, for: tv) #expect(try await store.validateCertificate(sha256: first, for: tv) == .accepted) #expect(try await store.validateCertificate(sha256: changed, for: tv) == .requiresReapproval) #expect((try await store.authorization(for: tv)).trust == .pinnedLeafCertificateSHA256(first)) } @Test func revokePreservesTrustButForgetRemovesBoth() async throws { let client = MockKeychainClient() let store = KeychainTVAuthorizationStore(client: client) let tv = UUID() let fingerprint = Data(repeating: 0x03, count: 32) try await store.saveToken("token", for: tv) try await store.approveCertificate(sha256: fingerprint, for: tv) try await store.deleteToken(for: tv) #expect((try await store.authorization(for: tv)).token == nil) #expect(try await store.validateCertificate(sha256: fingerprint, for: tv) == .requiresReapproval) try await store.forgetTelevision(tv) #expect(try await store.securityRecord(for: tv) == nil) } @Test func lockedAndUnexpectedSecurityErrorsArePropagated() async throws { let client = MockKeychainClient() let store = KeychainTVAuthorizationStore(client: client) let tv = UUID() client.setFailure(.locked) await #expect(throws: KeychainError.locked) { try await store.approveCertificate(sha256: Data(repeating: 0, count: 32), for: tv) } client.setFailure(.unexpected) await #expect(throws: KeychainError.unexpectedStatus(-1)) { try await store.approveCertificate(sha256: Data(repeating: 0, count: 32), for: tv) } } } private extension MockKeychainClient { func setFailure(_ failure: Failure?) { self.failure = failure } } @Test func newCertificateApprovalCannotReusePreviousToken() async throws { let store = KeychainTVAuthorizationStore(client: MockKeychainClient()) let tv = UUID(), first = Data(repeating: 1, count: 32), changed = Data(repeating: 2, count: 32) try await store.approveCertificate(sha256: first, for: tv) try await store.saveToken("synthetic-token", for: tv) try await store.approveCertificate(sha256: changed, for: tv) let stored = try await store.authorization(for: tv) #expect(stored.token == nil) #expect(stored.trust == .pinnedLeafCertificateSHA256(changed)) } @Test func malformedStoredPinDoesNotFallBackToSystemTrust() async throws { let client = MockKeychainClient() let id = UUID() let invalid = TVSecurityRecord(token: "synthetic", certificateSHA256: Data([1])) try client.add(service: KeychainTVAuthorizationStore.service, account: id.uuidString, data: JSONEncoder().encode(invalid), accessibility: .whenUnlockedThisDeviceOnly) let store = KeychainTVAuthorizationStore(client: client) await #expect(throws: (any Error).self) { try await store.authorization(for: id) } }