Init
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user