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. An ipx API token (Settings, API tokens), which ipx reads /// as `Authorization: Bearer`, does not end by itself. Access would turn a request carrying only /// that away, so the player sends it to a second address with no Access in front, which /// Cloudflare's firewall holds to the paths the player uses (`Server.apiURL`). enum Credentials { private static let service = "net.sdf1.ipx" private static let account = "ipx.api-token" static var apiToken: String? { get { var query = base 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 as CFDictionary) guard let value = newValue?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return } var item = base 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 var base: [String: Any] { [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account] } /// Empty unless there is a token, and an address to send it to: sent to the page's own /// address it would only be turned away by Access. static var headers: [String: String] { guard let token = apiToken, Server.apiURL != nil else { return [:] } return ["Authorization": "Bearer \(token)"] } static var isSet: Bool { !headers.isEmpty } }