From 304d0215b610b708b2702c707d87f71a60a6d0e6 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 20 Dec 2020 10:26:28 +0100 Subject: [PATCH] Use keychain service as item context Primary key = (context, username) --- CHANGELOG.md | 1 + TunnelKit/Sources/AppExtension/Keychain.swift | 84 ++++++++----------- .../OpenVPNTunnelProvider+Configuration.swift | 4 +- 3 files changed, 36 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e59f3..63303fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update API to access current Wi-Fi SSID. +- Refactor access to keychain. ## 3.0.0 (2020-11-15) diff --git a/TunnelKit/Sources/AppExtension/Keychain.swift b/TunnelKit/Sources/AppExtension/Keychain.swift index 23340d4..3d4ac37 100644 --- a/TunnelKit/Sources/AppExtension/Keychain.swift +++ b/TunnelKit/Sources/AppExtension/Keychain.swift @@ -54,39 +54,18 @@ public enum KeychainError: Error { /// Wrapper for easy keychain access and modification. public class Keychain { - private let service: String? - private let accessGroup: String? - /// :nodoc: - public init() { - service = Bundle.main.bundleIdentifier - accessGroup = nil - } - /** - Creates a keychain in an App Group. + Creates a keychain. - - Parameter group: The App Group. - - Precondition: Proper App Group entitlements. + - Parameter group: An optional App Group. + - Precondition: Proper App Group entitlements (if group is non-nil). **/ - public init(group: String) { - service = nil + public init(group: String?) { accessGroup = group } - /** - Creates a keychain in an App Group and a Team ID prefix. - - - Parameter team: The Team ID prefix. - - Parameter group: The App Group. - - Precondition: Proper App Group entitlements. - **/ - public init(team: String, group: String) { - service = nil - accessGroup = "\(team).\(group)" - } - // MARK: Password /** @@ -94,16 +73,16 @@ public class Keychain { - Parameter password: The password to set. - Parameter username: The username to set the password for. - - Parameter label: An optional label. + - Parameter context: An optional context. - Throws: `KeychainError.add` if unable to add the password to the keychain. **/ - public func set(password: String, for username: String, label: String? = nil) throws { + public func set(password: String, for username: String, context: String? = nil) throws { do { - let currentPassword = try self.password(for: username) + let currentPassword = try self.password(for: username, context: context) guard password != currentPassword else { return } - removePassword(for: username) + removePassword(for: username, context: context) } catch let e as KeychainError { // rethrow cancelation @@ -115,17 +94,14 @@ public class Keychain { } var query = [String: Any]() - setScope(query: &query) + setScope(query: &query, context: context) query[kSecClass as String] = kSecClassGenericPassword - if let label = label { - query[kSecAttrLabel as String] = label - } query[kSecAttrAccount as String] = username query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock query[kSecValueData as String] = password.data(using: .utf8) - + let status = SecItemAdd(query as CFDictionary, nil) - guard (status == errSecSuccess) else { + guard status == errSecSuccess else { throw KeychainError.add } } @@ -134,28 +110,30 @@ public class Keychain { Removes a password. - Parameter username: The username to remove the password for. + - Parameter context: An optional context. - Returns: `true` if the password was successfully removed. **/ - @discardableResult public func removePassword(for username: String) -> Bool { + @discardableResult public func removePassword(for username: String, context: String? = nil) -> Bool { var query = [String: Any]() - setScope(query: &query) + setScope(query: &query, context: context) query[kSecClass as String] = kSecClassGenericPassword query[kSecAttrAccount as String] = username - + let status = SecItemDelete(query as CFDictionary) - return (status == errSecSuccess) + return status == errSecSuccess } /** Gets a password. - Parameter username: The username to get the password for. + - Parameter context: An optional context. - Returns: The password for the input username. - Throws: `KeychainError.notFound` if unable to find the password in the keychain. **/ - public func password(for username: String) throws -> String { + public func password(for username: String, context: String? = nil) throws -> String { var query = [String: Any]() - setScope(query: &query) + setScope(query: &query, context: context) query[kSecClass as String] = kSecClassGenericPassword query[kSecAttrAccount as String] = username query[kSecMatchLimit as String] = kSecMatchLimitOne @@ -185,12 +163,13 @@ public class Keychain { Gets a password reference. - Parameter username: The username to get the password for. + - Parameter context: An optional context. - Returns: The password reference for the input username. - Throws: `KeychainError.notFound` if unable to find the password in the keychain. **/ - public func passwordReference(for username: String) throws -> Data { + public func passwordReference(for username: String, context: String? = nil) throws -> Data { var query = [String: Any]() - setScope(query: &query) + setScope(query: &query, context: context) query[kSecClass as String] = kSecClassGenericPassword query[kSecAttrAccount as String] = username query[kSecMatchLimit as String] = kSecMatchLimitOne @@ -218,12 +197,16 @@ public class Keychain { - Parameter username: The username to get the password for. - Parameter reference: The password reference. + - Parameter context: An optional context. - Returns: The password for the input username and reference. - Throws: `KeychainError.notFound` if unable to find the password in the keychain. **/ - public static func password(for username: String, reference: Data) throws -> String { + public static func password(for username: String, reference: Data, context: String? = nil) throws -> String { var query = [String: Any]() query[kSecClass as String] = kSecClassGenericPassword + if let context = context { + query[kSecAttrService as String] = context + } query[kSecAttrAccount as String] = username query[kSecMatchItemList as String] = [reference] query[kSecReturnData as String] = true @@ -331,18 +314,17 @@ public class Keychain { query.removeValue(forKey: kSecAttrService as String) let status = SecItemDelete(query as CFDictionary) - return (status == errSecSuccess) + return status == errSecSuccess } // MARK: Helpers - private func setScope(query: inout [String: Any]) { - if let service = service { - query[kSecAttrService as String] = service - } else if let accessGroup = accessGroup { + private func setScope(query: inout [String: Any], context: String?) { + if let accessGroup = accessGroup { query[kSecAttrAccessGroup as String] = accessGroup - } else { - fatalError("No service nor accessGroup set") + } + if let context = context { + query[kSecAttrService as String] = context } } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift index f6c1c8f..e65a99e 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift @@ -403,12 +403,12 @@ extension OpenVPNTunnelProvider { if let username = credentials?.username, let password = credentials?.password { let keychain = Keychain(group: appGroup) do { - try keychain.set(password: password, for: username, label: Bundle.main.bundleIdentifier) + try keychain.set(password: password, for: username, context: bundleIdentifier) } catch _ { throw ProviderConfigurationError.credentials(details: "keychain.set()") } protocolConfiguration.username = username - protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username) + protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username, context: bundleIdentifier) } protocolConfiguration.providerConfiguration = generatedProviderConfiguration(appGroup: appGroup)