mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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.
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_ID)</string>
|
||||
<!--
|
||||
App group id was updated in 1.4.0. Can be removed after all clients have
|
||||
upgraded to 1.4.0.
|
||||
-->
|
||||
<string>$(APP_GROUP_ID_PRE_1_4_0)</string>
|
||||
</array>
|
||||
<key>com.apple.developer.system-extension.install</key>
|
||||
<true/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_ID)</string>
|
||||
<!--
|
||||
App group id was updated in 1.4.0. Can be removed after all clients have
|
||||
upgraded to 1.4.0.
|
||||
-->
|
||||
<string>$(APP_GROUP_ID_PRE_1_4_0)</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user