From ac608d560a5773a409bac3f8c4c03b9616a3066a Mon Sep 17 00:00:00 2001 From: Jamil Date: Sun, 8 Dec 2024 19:17:46 -0800 Subject: [PATCH] refactor(apple): Migrate firezone-id file to keychain (#7464) Unlike the App extension which runs as the user, the system extension introduced in macOS client 1.4.0 runs as `root` and thus cannot read the App Group container directory for the GUI process. However, both processes can read and write to the shared Keychain, which is how we pass the token between the two processes already. This PR does two things: 1. Tries to read an existing `firezone-id` from the pre-1.4.0 App Group container upon app launch. This needs to be done from the GUI process. If found, it stores it in the Keychain. 1. Refactors the `firezone-id` to be stored in the Keychain instead of a plaintext file going forward. The Keychain API is also cleaned up and abstracted to be more ergonomic to use for both Token and Firezone ID storage purposes. --- .../Firezone/Application/FirezoneApp.swift | 14 ++ swift/apple/Firezone/Firezone.entitlements | 5 + swift/apple/Firezone/xcconfig/debug.xcconfig | 2 + .../apple/Firezone/xcconfig/release.xcconfig | 2 + .../FirezoneKit/Helpers/DeviceMetadata.swift | 35 ----- .../FirezoneKit/Keychain/Keychain.swift | 79 +++-------- .../FirezoneKit/Models/FirezoneId.swift | 127 ++++++++++++++++++ .../Sources/FirezoneKit/Models/Token.swift | 80 +++++++++++ .../Sources/FirezoneKit/Views/AppView.swift | 15 ++- .../FirezoneNetworkExtension/Adapter.swift | 13 +- .../FirezoneNetworkExtension.entitlements | 5 + .../PacketTunnelProvider.swift | 67 +++------ 12 files changed, 293 insertions(+), 151 deletions(-) create mode 100644 swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift create mode 100644 swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift diff --git a/swift/apple/Firezone/Application/FirezoneApp.swift b/swift/apple/Firezone/Application/FirezoneApp.swift index d85853823..098aa2482 100644 --- a/swift/apple/Firezone/Application/FirezoneApp.swift +++ b/swift/apple/Firezone/Application/FirezoneApp.swift @@ -69,6 +69,20 @@ struct FirezoneApp: App { public var store: Store? func applicationDidFinishLaunching(_: Notification) { + + Task { + // In 1.4.0 and higher, the macOS client uses a system extension as its + // Network Extension packaging type. It runs as root and can't read the + // existing firezone-id file. So read it here from the app process instead + // and save it to the Keychain, where we should store shared persistent + // data going forward. + // + // Can be removed once all clients >= 1.4.0 + try await FirezoneId.migrate() + + try await FirezoneId.createIfMissing() + } + if let store = store { menuBar = MenuBar(model: SessionViewModel(favorites: favorites, store: store)) } diff --git a/swift/apple/Firezone/Firezone.entitlements b/swift/apple/Firezone/Firezone.entitlements index 92c8fb3e8..1c3620cc3 100644 --- a/swift/apple/Firezone/Firezone.entitlements +++ b/swift/apple/Firezone/Firezone.entitlements @@ -11,6 +11,11 @@ com.apple.security.application-groups $(APP_GROUP_ID) + + $(APP_GROUP_ID_PRE_1_4_0) com.apple.developer.system-extension.install diff --git a/swift/apple/Firezone/xcconfig/debug.xcconfig b/swift/apple/Firezone/xcconfig/debug.xcconfig index 40024bc3a..8b461dc5b 100644 --- a/swift/apple/Firezone/xcconfig/debug.xcconfig +++ b/swift/apple/Firezone/xcconfig/debug.xcconfig @@ -2,5 +2,7 @@ DEVELOPMENT_TEAM = 47R2M6779T PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.firezone APP_GROUP_ID[sdk=macosx*] = 47R2M6779T.dev.firezone.firezone +APP_GROUP_ID_PRE_1_4_0[sdk=macosx*] = 47R2M6779T.group.dev.firezone.firezone APP_GROUP_ID[sdk=iphoneos*] = group.dev.firezone.firezone +APP_GROUP_ID_PRE_1_4_0[sdk=iphoneos*] = group.dev.firezone.firezone CODE_SIGN_STYLE = Automatic diff --git a/swift/apple/Firezone/xcconfig/release.xcconfig b/swift/apple/Firezone/xcconfig/release.xcconfig index 2911175c8..60c40c9a3 100644 --- a/swift/apple/Firezone/xcconfig/release.xcconfig +++ b/swift/apple/Firezone/xcconfig/release.xcconfig @@ -2,7 +2,9 @@ DEVELOPMENT_TEAM = 47R2M6779T PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.firezone APP_GROUP_ID[sdk=macosx*] = 47R2M6779T.dev.firezone.firezone +APP_GROUP_ID_PRE_1_4_0[sdk=macosx*] = 47R2M6779T.group.dev.firezone.firezone APP_GROUP_ID[sdk=iphoneos*] = group.dev.firezone.firezone +APP_GROUP_ID_PRE_1_4_0[sdk=iphoneos*] = group.dev.firezone.firezone CODE_SIGN_STYLE = Manual CODE_SIGN_IDENTITY = Apple Distribution: Firezone, Inc. (47R2M6779T) IOS_APP_PROVISIONING_PROFILE_IDENTIFIER = 07102026-065f-4cc0-800b-5f8595c50ce8 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift index 71e75887e..44bd8eb18 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift @@ -12,16 +12,6 @@ import UIKit #endif public class DeviceMetadata { - // If firezone-id hasn't ever been written, the app is considered - // to be launched for the first time. - public static func firstTime() -> Bool { - let fileExists = FileManager.default.fileExists( - atPath: SharedAccess.baseFolderURL.appendingPathComponent("firezone-id").path - ) - - return !fileExists - } - public static func getDeviceName() -> String { // Returns a generic device name on iOS 16 and higher // See https://github.com/firezone/firezone/issues/3034 @@ -42,31 +32,6 @@ public class DeviceMetadata { return "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)" } - // Returns the Firezone ID as cached by the application or generates and persists a new one - // if that doesn't exist. The Firezone ID is a UUIDv4 that is used to dedup this device - // for upsert and identification in the admin portal. - public static func getOrCreateFirezoneId() -> String { - let fileURL = SharedAccess.baseFolderURL.appendingPathComponent("firezone-id") - - do { - return try String(contentsOf: fileURL, encoding: .utf8) - } catch { - // Handle the error if the file doesn't exist or isn't readable - // Recreate the file, save a new UUIDv4, and return it - let newUUIDString = UUID().uuidString - - do { - try newUUIDString.write(to: fileURL, atomically: true, encoding: .utf8) - } catch { - Log.app.error( - "\(#function): Could not save firezone-id file \(fileURL.path)! Error: \(error)" - ) - } - - return newUUIDString - } - } - #if os(iOS) public static func deviceInfo() -> DeviceInfo { return DeviceInfo(identifierForVendor: UIDevice.current.identifierForVendor!.uuidString) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift index d854a55d9..6a4f89bd1 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift @@ -18,17 +18,9 @@ public enum KeychainError: Error { } public actor Keychain { - private let label = "Firezone token" - private let description = "Firezone access token used to authenticate the client." - private let service = Bundle.main.bundleIdentifier! - - // Bump this for backwards-incompatible Keychain changes; this is effectively the - // upsert key. - private let account = "1" - + public static let shared = Keychain() private let workQueue = DispatchQueue(label: "FirezoneKeychainWorkQueue") - public typealias Token = String public typealias PersistentRef = Data public enum SecStatus: Equatable { @@ -50,23 +42,9 @@ public actor Keychain { public init() {} - public func add(token: Token) async throws { + public func add(query: [CFString: Any]) async throws { return try await withCheckedThrowingContinuation { [weak self] continuation in - self?.workQueue.async { [weak self] in - guard let self = self else { - continuation.resume(throwing: KeychainError.securityError(.unexpectedError)) - return - } - - let query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrLabel: self.label, - kSecAttrAccount: self.account, - kSecAttrDescription: self.description, - kSecAttrService: self.service, - kSecValueData: token.data(using: .utf8) as Any, - ] - + self?.workQueue.async { var ref: CFTypeRef? let ret = SecStatus(SecItemAdd(query as CFDictionary, &ref)) guard ret.isSuccess else { @@ -81,24 +59,12 @@ public actor Keychain { } } - public func update(token: Token) async throws { + public func update( + query: [CFString: Any], + attributesToUpdate: [CFString: Any] + ) async throws { return try await withCheckedThrowingContinuation { [weak self] continuation in - self?.workQueue.async { [weak self] in - guard let self = self else { - continuation.resume(throwing: KeychainError.securityError(.unexpectedError)) - return - } - - let query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrLabel: self.label, - kSecAttrAccount: self.account, - kSecAttrDescription: self.description, - kSecAttrService: self.service, - ] - let attributesToUpdate = [ - kSecValueData: token.data(using: .utf8) as Any - ] + self?.workQueue.async { let ret = SecStatus( SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)) guard ret.isSuccess else { @@ -111,8 +77,7 @@ public actor Keychain { } } - // This function is public because the tunnel needs to call it to get the token - public func load(persistentRef: PersistentRef) async -> Token? { + public func load(persistentRef: PersistentRef) async -> Data? { return await withCheckedContinuation { [weak self] continuation in self?.workQueue.async { let query = @@ -124,10 +89,9 @@ public actor Keychain { var result: CFTypeRef? let ret = SecStatus(SecItemCopyMatching(query as CFDictionary, &result)) if ret.isSuccess, - let resultData = result as? Data, - let resultString = String(data: resultData, encoding: .utf8) + let resultData = result as? Data { - continuation.resume(returning: resultString) + continuation.resume(returning: resultData) } else { continuation.resume(returning: nil) } @@ -135,23 +99,20 @@ public actor Keychain { } } - public func search() async -> PersistentRef? { + public func search(query: [CFString: Any]) async -> PersistentRef? { return await withCheckedContinuation { [weak self] continuation in guard let self = self else { return } self.workQueue.async { - let query = - [ - kSecClass: kSecClassGenericPassword, - kSecAttrLabel: self.label, - kSecAttrAccount: self.account, - kSecAttrDescription: self.description, - kSecAttrService: self.service, - kSecReturnPersistentRef: true, - ] as [CFString: Any] + let query = query.merging([ + kSecClass: kSecClassGenericPassword, + kSecReturnPersistentRef: true, + ]) { (current, new) in new } + var result: CFTypeRef? let ret = SecStatus(SecItemCopyMatching(query as CFDictionary, &result)) - if ret.isSuccess, let tokenRef = result as? Data { - continuation.resume(returning: tokenRef) + + if ret.isSuccess, let persistentRef = result as? Data { + continuation.resume(returning: persistentRef) } else { continuation.resume(returning: nil) } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift new file mode 100644 index 000000000..91718b1d4 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift @@ -0,0 +1,127 @@ +// +// FirezoneId.swift +// (c) 2024 Firezone, Inc. +// LICENSE: Apache-2.0 +// +// Convenience wrapper for working with our firezone-id stored in the Keychain. + +import Foundation + +public struct FirezoneId { + private static let query: [CFString: Any] = [ + kSecAttrLabel: "Firezone id", + kSecAttrAccount: "2", + kSecAttrService: AppInfoPlistConstants.appGroupId, + kSecAttrDescription: "Firezone device id", + ] + + private var uuid: UUID + + public init(_ uuid: UUID? = nil) { + self.uuid = uuid ?? UUID() + } + + // Upsert the firezone-id to the Keychain + public func save(_ keychain: Keychain = Keychain.shared) async throws { + guard await keychain.search(query: FirezoneId.query) == nil + else { + let query = FirezoneId.query.merging([ + kSecClass: kSecClassGenericPassword + ]) { (_, new) in new } + return try await keychain.update( + query: query, + attributesToUpdate: [kSecValueData: uuid.toData()] + ) + } + + let query = FirezoneId.query.merging([ + kSecClass: kSecClassGenericPassword, + kSecValueData: uuid.toData() + ]) { (_, new) in new } + + try await keychain.add(query: query) + } + + // Attempt to load the firezone-id from the Keychain + public static func load(_ keychain: Keychain = Keychain.shared) async throws -> FirezoneId? { + guard let idRef = await keychain.search(query: query) + else { return nil } + + guard let data = await keychain.load(persistentRef: idRef) + else { return nil } + + guard data.count == UUID.sizeInBytes + else { + fatalError("Firezone ID loaded from keychain must be exactly \(UUID.sizeInBytes) bytes") + } + + let uuid = UUID(fromData: data) + return FirezoneId(uuid) + } + + // Prior to 1.4.0, our firezone-id was saved in a file. Starting with 1.4.0, + // the macOS client uses a system extension, which makes sharing folders with + // the app cumbersome, so we moved to using the keychain for this due to its + // better ergonomics. If the old firezone-id doesn't exist, this function + // is a no-op. + // + // Can be refactored to remove the file check once all clients >= 1.4.0 + public static func migrate() async throws { + guard try await load() == nil + else { return } // New firezone-id already saved in Keychain + +#if os(macOS) + let appGroupIdPre_1_4_0 = "47R2M6779T.group.dev.firezone.firezone" +#elseif os(iOS) + let appGroupIdPre_1_4_0 = "group.dev.firezone.firezone" +#endif + + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupIdPre_1_4_0) + else { fatalError("Couldn't find app group container") } + + let idFileURL = containerURL.appendingPathComponent("firezone-id") + + // If the file isn't there or can't be read, bail + guard FileManager.default.fileExists(atPath: idFileURL.path), + let uuidString = try? String(contentsOf: idFileURL) + else { return } + + let firezoneId = FirezoneId(UUID(uuidString: uuidString)) + try await firezoneId.save() + } + + public static func createIfMissing() async throws { + guard try await load() == nil + else { return } // New firezone-id already saved in Keychain + + let firezoneId = FirezoneId(UUID()) + try await firezoneId.save() + } +} + +// Convenience extension to convert to/from Data for storing in Keychain +extension UUID { + // We need the size of a UUID to (1) know how big to make the Data buffer, + // and (2) to make sure the UUID we read from the keychain is a valid length. + public static let sizeInBytes = MemoryLayout.size(ofValue: UUID()) + + init(fromData: Data) { + self = fromData.withUnsafeBytes { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress + else { + fatalError("Buffer should point to a valid memory address") + } + + return UUID(uuid: baseAddress.assumingMemoryBound(to: uuid_t.self).pointee) + } + } + + func toData() -> Data { + let data = withUnsafePointer(to: self) { rawBufferPinter in + Data(bytes: rawBufferPinter, count: UUID.sizeInBytes) + } + + return data + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift new file mode 100644 index 000000000..e7fe2b78c --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift @@ -0,0 +1,80 @@ +// +// Token.swift +// (c) 2024 Firezone, Inc. +// LICENSE: Apache-2.0 +// +// Convenience wrapper for working with our auth token stored in the Keychain. + +import Foundation + +public struct Token: CustomStringConvertible { + private static let query: [CFString: Any] = [ + kSecAttrLabel: "Firezone token", + kSecAttrAccount: "1", + kSecAttrService: AppInfoPlistConstants.appGroupId, + kSecAttrDescription: "Firezone access token", + ] + + private var data: Data + + public var description: String { String(data: data, encoding: .utf8)! } + + public init?(_ tokenString: String?) { + guard let tokenString = tokenString, + let data = tokenString.data(using: .utf8) + else { return nil } + + self.data = data + } + + public init(_ data: Data) { + self.data = data + } + + public static func delete( + _ keychain: Keychain = Keychain.shared + ) async throws { + + guard let tokenRef = await keychain.search(query: query) + else { return } + + try await keychain.delete(persistentRef: tokenRef) + } + + // Upsert token to Keychain + public func save(_ keychain: Keychain = Keychain.shared) async throws { + + guard await keychain.search(query: Token.query) == nil + else { + let query = Token.query.merging([ + kSecClass: kSecClassGenericPassword + ]) { (_, new) in new } + + return try await keychain.update( + query: query, + attributesToUpdate: [kSecValueData: data] + ) + } + + let query = Token.query.merging([ + kSecClass: kSecClassGenericPassword, + kSecValueData: data + ]) { (_, new) in new } + + try await keychain.add(query: query) + } + + // Attempt to load token from Keychain + public static func load( + _ keychain: Keychain = Keychain.shared + ) async throws -> Token? { + + guard let tokenRef = await keychain.search(query: query) + else { return nil } + + guard let data = await keychain.load(persistentRef: tokenRef) + else { return nil } + + return Token(data) + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift index 34aebf508..77ddf8720 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift @@ -41,10 +41,17 @@ public class AppViewModel: ObservableObject { self.status = status #if os(macOS) - if status == .invalid || DeviceMetadata.firstTime() { - AppViewModel.WindowDefinition.main.openWindow() - } else { - AppViewModel.WindowDefinition.main.window()?.close() + Task { + let firezoneId = try await FirezoneId.load() + + if status == .invalid || firezoneId == nil { + + // Show the Wecome view if VPN permission needs to be granted + // or it's the first time starting + AppViewModel.WindowDefinition.main.openWindow() + } else { + AppViewModel.WindowDefinition.main.window()?.close() + } } #endif }) diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index 30d9f7aec..d516427f3 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -78,13 +78,13 @@ class Adapter { /// Starting parameters private var apiURL: String - private var token: String + private var token: Token private let logFilter: String private let connlibLogFolderPath: String init( apiURL: String, - token: String, + token: Token, logFilter: String, internetResourceEnabled: Bool, packetTunnelProvider: PacketTunnelProvider @@ -115,7 +115,7 @@ class Adapter { } /// Start the tunnel. - public func start() throws { + public func start() async throws { Log.tunnel.log("Adapter.start") guard case .tunnelStopped = self.state else { throw AdapterError.invalidState @@ -131,12 +131,15 @@ class Adapter { do { let jsonEncoder = JSONEncoder() jsonEncoder.keyEncodingStrategy = .convertToSnakeCase + + let firezoneId = try await FirezoneId.load() + // Grab a session pointer let session = try WrappedSession.connect( apiURL, - token, - DeviceMetadata.getOrCreateFirezoneId(), + "\(token)", + "\(firezoneId!)", DeviceMetadata.getDeviceName(), DeviceMetadata.getOSVersion(), connlibLogFolderPath, diff --git a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension.entitlements b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension.entitlements index 754f9edf6..bf13ab5f8 100644 --- a/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension.entitlements +++ b/swift/apple/FirezoneNetworkExtension/FirezoneNetworkExtension.entitlements @@ -9,6 +9,11 @@ com.apple.security.application-groups $(APP_GROUP_ID) + + $(APP_GROUP_ID_PRE_1_4_0) com.apple.security.app-sandbox diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index 9125ae518..6ca33e236 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -25,35 +25,28 @@ class PacketTunnelProvider: NEPacketTunnelProvider { Task { do { - var token = options?["token"] as? String - let keychain = Keychain() - let tokenRef = await keychain.search() + // Can be removed after all clients >= 1.4.0 + try await FirezoneId.migrate() - if let token = token { - // 1. If we're passed a token, save it to keychain + // The tunnel can come up without the app having been launched first, so + // initialize the id here too. + try await FirezoneId.createIfMissing() - // Apple recommends updating Keychain items in place if possible - // In reality this won't happen unless there's some kind of race condition - // because we would have deleted the item upon sign out. - if tokenRef != nil { - try await keychain.update(token: token) - } else { - try await keychain.add(token: token) - } + var passedToken = options?["token"] as? String + var keychainToken = try await Token.load() - } else { + // Use the provided token or try loading one from the Keychain + guard let token = Token(passedToken) ?? keychainToken + else { + completionHandler(PacketTunnelProviderError.tokenNotFoundInKeychain) - // 2. Otherwise, load it from the keychain - guard let tokenRef = tokenRef - else { - completionHandler(PacketTunnelProviderError.tokenNotFoundInKeychain) - return - } - - token = await keychain.load(persistentRef: tokenRef) + return } - // 3. Now we should have a token, so connect + // Save the token back to the Keychain + try await token.save() + + // Now we should have a token, so continue connecting guard let apiURL = protocolConfiguration.serverAddress else { completionHandler( @@ -72,12 +65,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - guard let token = token - else { - completionHandler(PacketTunnelProviderError.tokenNotFoundInKeychain) - return - } - let internetResourceEnabled: Bool = if let internetResourceEnabledJSON = providerConfiguration[TunnelManagerKeys.internetResourceEnabled]?.data(using: .utf8) { (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabledJSON )) ?? false } else { @@ -89,7 +76,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.adapter = adapter - try adapter.start() + try await adapter.start() // Tell the system the tunnel is up, moving the tunnelManager status to // `connected`. @@ -112,7 +99,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { if case .authenticationCanceled = reason { do { // This was triggered from onDisconnect, so clear our token - Task { await clearToken() } + Task { try await Token.delete() } // There's no good way to send data like this from the // Network Extension to the GUI, so save it to a file for the GUI to read upon @@ -145,7 +132,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { adapter?.setInternetResourceEnabled(value) case .signOut: Task { - await clearToken() + try await Token.delete() } case .getResourceList(let value): adapter?.getResourcesIfVersionDifferentFrom(hash: value) { @@ -154,22 +141,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } } - - enum TokenError: Error { - case TokenNotFound - } - - private func clearToken() async { - do { - let keychain = Keychain() - guard let ref = await keychain.search() else { - throw TokenError.TokenNotFound - } - try await keychain.delete(persistentRef: ref) - } catch { - Log.tunnel.error("\(#function): Error: \(error)") - } - } } extension NEProviderStopReason: CustomStringConvertible {