import Foundation import Security /// What the player and CarPlay send instead of the web view's cookie (#419). /// /// The cookie Cloudflare Access leaves in the web view lasts as long as its session, and when it /// ends mid-drive the player has nothing. Two things that do not end by themselves replace it: /// an ipx API token (Settings, API tokens), which ipx reads as `Authorization: Bearer`, and an /// Access service token (Zero Trust, Service Auth), which Cloudflare reads as /// `CF-Access-Client-Id` and `CF-Access-Client-Secret` before the request reaches ipx. Either /// half alone is refused somewhere, so `headers` is empty unless it has all three, and the player /// then falls back to the cookie as before. enum Credentials { enum Field: String, CaseIterable { case apiToken = "ipx.api-token" case accessId = "ipx.access-client-id" case accessSecret = "ipx.access-client-secret" } private static let service = "net.sdf1.ipx" static subscript(field: Field) -> String? { get { var query = base(field) query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne var out: CFTypeRef? guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess, let data = out as? Data else { return nil } return String(data: data, encoding: .utf8) } set { SecItemDelete(base(field) as CFDictionary) guard let value = newValue?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return } var item = base(field) item[kSecValueData as String] = Data(value.utf8) // After first unlock, not when unlocked: CarPlay and a locked phone both play. item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock SecItemAdd(item as CFDictionary, nil) } } private static func base(_ field: Field) -> [String: Any] { [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: field.rawValue] } /// The headers for a request to the server, or none when any of the three is missing. static var headers: [String: String] { guard let token = self[.apiToken], let id = self[.accessId], let secret = self[.accessSecret] else { return [:] } return ["Authorization": "Bearer \(token)", "CF-Access-Client-Id": id, "CF-Access-Client-Secret": secret] } static var isSet: Bool { !headers.isEmpty } }