Keychain: store configurations in keychain instead of providerConfig
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
private static var sharedFolderURL: URL? {
|
||||
static var appGroupId: String? {
|
||||
#if os(iOS)
|
||||
let appGroupIdInfoDictionaryKey = "com.wireguard.ios.app_group_id"
|
||||
#elseif os(macOS)
|
||||
@@ -13,7 +13,10 @@ extension FileManager {
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: appGroupIdInfoDictionaryKey) as? String else {
|
||||
return Bundle.main.object(forInfoDictionaryKey: appGroupIdInfoDictionaryKey) as? String
|
||||
}
|
||||
private static var sharedFolderURL: URL? {
|
||||
guard let appGroupId = FileManager.appGroupId else {
|
||||
os_log("Cannot obtain app group ID from bundle", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class Keychain {
|
||||
static func openReference(called ref: Data) -> String? {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref,
|
||||
kSecReturnData as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
guard let data = result as? Data else { return nil }
|
||||
return String(data: data, encoding: String.Encoding.utf8)
|
||||
}
|
||||
|
||||
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
|
||||
var ret: OSStatus
|
||||
guard var id = Bundle.main.bundleIdentifier else {
|
||||
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
|
||||
return nil
|
||||
}
|
||||
if id.hasSuffix(".network-extension") {
|
||||
id.removeLast(".network-extension".count)
|
||||
}
|
||||
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
|
||||
kSecAttrAccount as String: name + ": " + UUID().uuidString,
|
||||
kSecAttrDescription as String: "wg-quick(8) config",
|
||||
kSecAttrService as String: id,
|
||||
kSecValueData as String: value.data(using: .utf8) as Any,
|
||||
kSecReturnPersistentRef as String: true]
|
||||
|
||||
#if os(iOS)
|
||||
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
#elseif os(macOS)
|
||||
items[kSecAttrSynchronizable as String] = false
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
|
||||
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else {
|
||||
wg_log(.error, staticMessage: "Unable to determine app extension path")
|
||||
return nil
|
||||
}
|
||||
var extensionApp: SecTrustedApplication?
|
||||
var mainApp: SecTrustedApplication?
|
||||
ret = SecTrustedApplicationCreateFromPath(extensionPath, &extensionApp)
|
||||
if ret != kOSReturnSuccess || extensionApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain extension trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
ret = SecTrustedApplicationCreateFromPath(nil, &mainApp)
|
||||
if ret != errSecSuccess || mainApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain local trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
var access: SecAccess?
|
||||
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
|
||||
[extensionApp!, mainApp!] as CFArray,
|
||||
&access)
|
||||
if ret != errSecSuccess || access == nil {
|
||||
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
items[kSecAttrAccess as String] = access!
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
|
||||
var ref: CFTypeRef?
|
||||
ret = SecItemAdd(items as CFDictionary, &ref)
|
||||
if ret != errSecSuccess || ref == nil {
|
||||
wg_log(.error, message: "Unable to add config to keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
if let oldRef = oldRef {
|
||||
deleteReference(called: oldRef)
|
||||
}
|
||||
return ref as? Data
|
||||
}
|
||||
|
||||
static func deleteReference(called ref: Data) {
|
||||
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary)
|
||||
if ret != errSecSuccess {
|
||||
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
||||
}
|
||||
}
|
||||
|
||||
static func deleteReferences(except whitelist: Set<Data>) {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnPersistentRef as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
return
|
||||
}
|
||||
guard let items = result as? [Data] else { return }
|
||||
for item in items {
|
||||
if !whitelist.contains(item) {
|
||||
deleteReference(called: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func verifyReference(called ref: Data) -> Bool {
|
||||
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref] as CFDictionary,
|
||||
nil) == errSecSuccess
|
||||
}
|
||||
}
|
||||
@@ -174,20 +174,32 @@ final class LegacyTunnelConfiguration: LegacyModel {
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
@discardableResult
|
||||
func migrateConfigurationIfNeeded() -> Bool {
|
||||
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
|
||||
if configurationVersion == 1 {
|
||||
migrateFromConfigurationV1()
|
||||
} else {
|
||||
fatalError("No migration from configuration version \(configurationVersion) exists.")
|
||||
func migrateConfigurationIfNeeded(called name: String) -> Bool {
|
||||
var ret = false
|
||||
if migrateFromConfigurationV1() {
|
||||
ret = true
|
||||
}
|
||||
if migrateFromConfigurationV2(called: name) {
|
||||
ret = true
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private func migrateFromConfigurationV1() -> Bool {
|
||||
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
|
||||
guard configurationVersion == 1 else { return false }
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return false }
|
||||
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return false }
|
||||
providerConfiguration = ["WgQuickConfig": configuration.migrated.asWgQuickConfig()]
|
||||
return true
|
||||
}
|
||||
|
||||
private func migrateFromConfigurationV1() {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return }
|
||||
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return }
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: configuration.migrated.asWgQuickConfig()]
|
||||
private func migrateFromConfigurationV2(called name: String) -> Bool {
|
||||
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
|
||||
providerConfiguration = nil
|
||||
guard passwordReference == nil else { return true }
|
||||
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,17 +12,16 @@ enum PacketTunnelProviderError: String, Error {
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
enum Keys: String {
|
||||
case wgQuickConfig = "WgQuickConfig"
|
||||
}
|
||||
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration, previouslyFrom old: NEVPNProtocol? = nil) {
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
guard let name = tunnelConfiguration.name else { return nil }
|
||||
guard let appId = Bundle.main.bundleIdentifier else { return nil }
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: tunnelConfiguration.asWgQuickConfig()]
|
||||
passwordReference = Keychain.makeReference(containing: tunnelConfiguration.asWgQuickConfig(), called: name, previouslyReferencedBy: old?.passwordReference)
|
||||
if passwordReference == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||
if endpoints.count == 1 {
|
||||
@@ -35,9 +34,26 @@ extension NETunnelProviderProtocol {
|
||||
}
|
||||
|
||||
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
|
||||
migrateConfigurationIfNeeded()
|
||||
guard let serializedConfig = providerConfiguration?[Keys.wgQuickConfig.rawValue] as? String else { return nil }
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: serializedConfig, called: name)
|
||||
migrateConfigurationIfNeeded(called: name ?? "unknown")
|
||||
//TODO: in the case where migrateConfigurationIfNeeded is called by the network extension,
|
||||
// before the app has started, and when there is, in fact, configuration that needs to be
|
||||
// put into the keychain, this will generate one new keychain item every time it is started,
|
||||
// until finally the app is open. Would it be possible to call saveToPreferences here? Or is
|
||||
// that generally not available to network extensions? In which case, what should our
|
||||
// behavior be?
|
||||
|
||||
guard let passwordReference = passwordReference else { return nil }
|
||||
guard let config = Keychain.openReference(called: passwordReference) else { return nil }
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
|
||||
}
|
||||
|
||||
func destroyConfigurationReference() {
|
||||
guard let ref = passwordReference else { return }
|
||||
Keychain.deleteReference(called: ref)
|
||||
}
|
||||
|
||||
func verifyConfigurationReference() -> Data? {
|
||||
guard let ref = passwordReference else { return nil }
|
||||
return Keychain.verifyReference(called: ref) ? ref : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user