Files
wristivo/Adapters/SecureStorage/Tests/SecureStorageTests/KeychainTVAuthorizationStoreTests.swift
T

120 lines
5.6 KiB
Swift

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) }
}