84 lines
3.7 KiB
Swift
84 lines
3.7 KiB
Swift
import Foundation
|
|||
|
|
import Security
|
||
|
|
import os.log
|
||
|
|
|
||
|
|
/// Authentication shared with the containing app through the Keychain access group.
|
||
|
|
/// The extension accesses the bearer token and pinned certificate fingerprint stored by the app.
|
||
|
|
enum PortalAuth {
|
||
|
|
static let suite = "ENT9X9U544.com.kovtash.portalcam"
|
||
|
|
static let accessGroup = "ENT9X9U544.com.kovtash.portalcam"
|
||
|
|
static let service = "com.kovtash.portalcam.auth"
|
||
|
|
static let tokenKey = "portalAuthToken"
|
||
|
|
static let certKey = "portalPinnedCertSha256"
|
||
|
|
|
||
|
|
static var token: String? {
|
||
|
|
return readItem(account: tokenKey)
|
||
|
|
}
|
||
|
|
|
||
|
|
static var pinnedCertSha256: String? {
|
||
|
|
return readItem(account: certKey)
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func readItem(account: String) -> String? {
|
||
|
|
// 1. Try modern Data Protection Keychain with explicit access group
|
||
|
|
let dpQuery: [String: Any] = [
|
||
|
|
kSecClass as String: kSecClassGenericPassword,
|
||
|
|
kSecAttrService as String: service,
|
||
|
|
kSecAttrAccount as String: account,
|
||
|
|
kSecAttrAccessGroup as String: accessGroup,
|
||
|
|
kSecReturnData as String: true,
|
||
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
||
|
|
kSecUseDataProtectionKeychain as String: true
|
||
|
|
]
|
||
|
|
var item: CFTypeRef?
|
||
|
|
let dpStatus = SecItemCopyMatching(dpQuery as CFDictionary, &item)
|
||
|
|
if dpStatus == errSecSuccess, let data = item as? Data,
|
||
|
|
let value = String(data: data, encoding: .utf8) {
|
||
|
|
os_log(.default, "PortalCam extension: %{public}@ found in DP Keychain with access group", account)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Try Data Protection Keychain without explicit access group (matches all groups in entitlement)
|
||
|
|
let defaultGroupQuery: [String: Any] = [
|
||
|
|
kSecClass as String: kSecClassGenericPassword,
|
||
|
|
kSecAttrService as String: service,
|
||
|
|
kSecAttrAccount as String: account,
|
||
|
|
kSecReturnData as String: true,
|
||
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
||
|
|
kSecUseDataProtectionKeychain as String: true
|
||
|
|
]
|
||
|
|
var defaultItem: CFTypeRef?
|
||
|
|
let defaultStatus = SecItemCopyMatching(defaultGroupQuery as CFDictionary, &defaultItem)
|
||
|
|
if defaultStatus == errSecSuccess, let data = defaultItem as? Data,
|
||
|
|
let value = String(data: data, encoding: .utf8) {
|
||
|
|
os_log(.default, "PortalCam extension: %{public}@ found in DP Keychain default group", account)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Try legacy file-based keychain
|
||
|
|
let legacyQuery: [String: Any] = [
|
||
|
|
kSecClass as String: kSecClassGenericPassword,
|
||
|
|
kSecAttrService as String: service,
|
||
|
|
kSecAttrAccount as String: account,
|
||
|
|
kSecReturnData as String: true,
|
||
|
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||
|
|
]
|
||
|
|
var legacyItem: CFTypeRef?
|
||
|
|
let legacyStatus = SecItemCopyMatching(legacyQuery as CFDictionary, &legacyItem)
|
||
|
|
if legacyStatus == errSecSuccess, let data = legacyItem as? Data,
|
||
|
|
let value = String(data: data, encoding: .utf8) {
|
||
|
|
os_log(.default, "PortalCam extension: %{public}@ found in Legacy Keychain", account)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Compatibility fallback: App Group UserDefaults
|
||
|
|
if let value = UserDefaults(suiteName: suite)?.string(forKey: account) {
|
||
|
|
os_log(.default, "PortalCam extension: %{public}@ found in App Group fallback", account)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
os_log(.default, "PortalCam extension: %{public}@ unavailable (DP status: %{public}d, default status: %{public}d, legacy status: %{public}d)", account, dpStatus, defaultStatus, legacyStatus)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
}
|