120 lines
5.6 KiB
Swift
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) }
|
|
}
|