Integrate live phone and watch remote prototype with guarded motion control
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
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) }
|
||||
}
|
||||
Reference in New Issue
Block a user