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 {