mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-26 10:05:54 +00:00
121 lines
4.1 KiB
Swift
121 lines
4.1 KiB
Swift
import Foundation
|
|
import RuViewNLOSCore
|
|
|
|
#if canImport(Security)
|
|
import Security
|
|
|
|
public enum PairingTokenStoreError: Error, LocalizedError, Sendable {
|
|
case keychainFailure(OSStatus)
|
|
case invalidStoredValue
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .keychainFailure:
|
|
return "The pairing token could not be accessed in Keychain."
|
|
case .invalidStoredValue:
|
|
return "The stored pairing token is invalid."
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class KeychainPairingTokenStore: @unchecked Sendable {
|
|
private let service: String
|
|
|
|
public init(service: String = "org.ruvnet.RuViewNLOS.pairing") {
|
|
self.service = service
|
|
}
|
|
|
|
public func save(_ token: String, for endpoint: URL) throws {
|
|
try WSSConnectionValidator.validatePairingToken(token)
|
|
let account = try WSSConnectionValidator.credentialAccount(for: endpoint)
|
|
guard let data = token.data(using: .utf8) else {
|
|
throw PairingTokenStoreError.invalidStoredValue
|
|
}
|
|
|
|
let identity: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: account,
|
|
]
|
|
let attributes: [String: Any] = [
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
]
|
|
|
|
let updateStatus = SecItemUpdate(identity as CFDictionary, attributes as CFDictionary)
|
|
if updateStatus == errSecSuccess { return }
|
|
guard updateStatus == errSecItemNotFound else {
|
|
throw PairingTokenStoreError.keychainFailure(updateStatus)
|
|
}
|
|
|
|
var insert = identity
|
|
attributes.forEach { insert[$0.key] = $0.value }
|
|
let insertStatus = SecItemAdd(insert as CFDictionary, nil)
|
|
guard insertStatus == errSecSuccess else {
|
|
throw PairingTokenStoreError.keychainFailure(insertStatus)
|
|
}
|
|
}
|
|
|
|
public func load(for endpoint: URL) throws -> String? {
|
|
let account = try WSSConnectionValidator.credentialAccount(for: endpoint)
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: account,
|
|
kSecReturnData as String: true,
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
]
|
|
var item: CFTypeRef?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
if status == errSecItemNotFound { return nil }
|
|
guard status == errSecSuccess, let data = item as? Data,
|
|
let token = String(data: data, encoding: .utf8) else {
|
|
if status == errSecSuccess {
|
|
throw PairingTokenStoreError.invalidStoredValue
|
|
}
|
|
throw PairingTokenStoreError.keychainFailure(status)
|
|
}
|
|
do {
|
|
try WSSConnectionValidator.validatePairingToken(token)
|
|
} catch {
|
|
throw PairingTokenStoreError.invalidStoredValue
|
|
}
|
|
return token
|
|
}
|
|
|
|
public func deleteAll() throws {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
]
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
|
throw PairingTokenStoreError.keychainFailure(status)
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
public enum PairingTokenStoreError: Error, LocalizedError, Sendable {
|
|
case unavailable
|
|
|
|
public var errorDescription: String? {
|
|
"Apple Keychain is unavailable on this platform."
|
|
}
|
|
}
|
|
|
|
public final class KeychainPairingTokenStore: @unchecked Sendable {
|
|
public init(service: String = "org.ruvnet.RuViewNLOS.pairing") {}
|
|
|
|
public func save(_ token: String, for endpoint: URL) throws {
|
|
throw PairingTokenStoreError.unavailable
|
|
}
|
|
public func load(for endpoint: URL) throws -> String? {
|
|
throw PairingTokenStoreError.unavailable
|
|
}
|
|
public func deleteAll() throws { throw PairingTokenStoreError.unavailable }
|
|
}
|
|
|
|
#endif
|