Use keychain service as item context

Primary key = (context, username)
This commit is contained in:
Davide De Rosa 2020-12-20 10:26:28 +01:00
parent 4a5bc92fcb
commit 304d0215b6
3 changed files with 36 additions and 53 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Update API to access current Wi-Fi SSID. - Update API to access current Wi-Fi SSID.
- Refactor access to keychain.
## 3.0.0 (2020-11-15) ## 3.0.0 (2020-11-15)

View File

@ -54,39 +54,18 @@ public enum KeychainError: Error {
/// Wrapper for easy keychain access and modification. /// Wrapper for easy keychain access and modification.
public class Keychain { public class Keychain {
private let service: String?
private let accessGroup: 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. - Parameter group: An optional App Group.
- Precondition: Proper App Group entitlements. - Precondition: Proper App Group entitlements (if group is non-nil).
**/ **/
public init(group: String) { public init(group: String?) {
service = nil
accessGroup = group 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 // MARK: Password
/** /**
@ -94,16 +73,16 @@ public class Keychain {
- Parameter password: The password to set. - Parameter password: The password to set.
- Parameter username: The username to set the password for. - 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. - 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 { do {
let currentPassword = try self.password(for: username) let currentPassword = try self.password(for: username, context: context)
guard password != currentPassword else { guard password != currentPassword else {
return return
} }
removePassword(for: username) removePassword(for: username, context: context)
} catch let e as KeychainError { } catch let e as KeychainError {
// rethrow cancelation // rethrow cancelation
@ -115,17 +94,14 @@ public class Keychain {
} }
var query = [String: Any]() var query = [String: Any]()
setScope(query: &query) setScope(query: &query, context: context)
query[kSecClass as String] = kSecClassGenericPassword query[kSecClass as String] = kSecClassGenericPassword
if let label = label {
query[kSecAttrLabel as String] = label
}
query[kSecAttrAccount as String] = username query[kSecAttrAccount as String] = username
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
query[kSecValueData as String] = password.data(using: .utf8) query[kSecValueData as String] = password.data(using: .utf8)
let status = SecItemAdd(query as CFDictionary, nil) let status = SecItemAdd(query as CFDictionary, nil)
guard (status == errSecSuccess) else { guard status == errSecSuccess else {
throw KeychainError.add throw KeychainError.add
} }
} }
@ -134,28 +110,30 @@ public class Keychain {
Removes a password. Removes a password.
- Parameter username: The username to remove the password for. - Parameter username: The username to remove the password for.
- Parameter context: An optional context.
- Returns: `true` if the password was successfully removed. - 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]() var query = [String: Any]()
setScope(query: &query) setScope(query: &query, context: context)
query[kSecClass as String] = kSecClassGenericPassword query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = username query[kSecAttrAccount as String] = username
let status = SecItemDelete(query as CFDictionary) let status = SecItemDelete(query as CFDictionary)
return (status == errSecSuccess) return status == errSecSuccess
} }
/** /**
Gets a password. Gets a password.
- Parameter username: The username to get the password for. - Parameter username: The username to get the password for.
- Parameter context: An optional context.
- Returns: The password for the input username. - Returns: The password for the input username.
- Throws: `KeychainError.notFound` if unable to find the password in the keychain. - 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]() var query = [String: Any]()
setScope(query: &query) setScope(query: &query, context: context)
query[kSecClass as String] = kSecClassGenericPassword query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = username query[kSecAttrAccount as String] = username
query[kSecMatchLimit as String] = kSecMatchLimitOne query[kSecMatchLimit as String] = kSecMatchLimitOne
@ -185,12 +163,13 @@ public class Keychain {
Gets a password reference. Gets a password reference.
- Parameter username: The username to get the password for. - Parameter username: The username to get the password for.
- Parameter context: An optional context.
- Returns: The password reference for the input username. - Returns: The password reference for the input username.
- Throws: `KeychainError.notFound` if unable to find the password in the keychain. - 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]() var query = [String: Any]()
setScope(query: &query) setScope(query: &query, context: context)
query[kSecClass as String] = kSecClassGenericPassword query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = username query[kSecAttrAccount as String] = username
query[kSecMatchLimit as String] = kSecMatchLimitOne query[kSecMatchLimit as String] = kSecMatchLimitOne
@ -218,12 +197,16 @@ public class Keychain {
- Parameter username: The username to get the password for. - Parameter username: The username to get the password for.
- Parameter reference: The password reference. - Parameter reference: The password reference.
- Parameter context: An optional context.
- Returns: The password for the input username and reference. - Returns: The password for the input username and reference.
- Throws: `KeychainError.notFound` if unable to find the password in the keychain. - 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]() var query = [String: Any]()
query[kSecClass as String] = kSecClassGenericPassword query[kSecClass as String] = kSecClassGenericPassword
if let context = context {
query[kSecAttrService as String] = context
}
query[kSecAttrAccount as String] = username query[kSecAttrAccount as String] = username
query[kSecMatchItemList as String] = [reference] query[kSecMatchItemList as String] = [reference]
query[kSecReturnData as String] = true query[kSecReturnData as String] = true
@ -331,18 +314,17 @@ public class Keychain {
query.removeValue(forKey: kSecAttrService as String) query.removeValue(forKey: kSecAttrService as String)
let status = SecItemDelete(query as CFDictionary) let status = SecItemDelete(query as CFDictionary)
return (status == errSecSuccess) return status == errSecSuccess
} }
// MARK: Helpers // MARK: Helpers
private func setScope(query: inout [String: Any]) { private func setScope(query: inout [String: Any], context: String?) {
if let service = service { if let accessGroup = accessGroup {
query[kSecAttrService as String] = service
} else if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup query[kSecAttrAccessGroup as String] = accessGroup
} else { }
fatalError("No service nor accessGroup set") if let context = context {
query[kSecAttrService as String] = context
} }
} }
} }

View File

@ -403,12 +403,12 @@ extension OpenVPNTunnelProvider {
if let username = credentials?.username, let password = credentials?.password { if let username = credentials?.username, let password = credentials?.password {
let keychain = Keychain(group: appGroup) let keychain = Keychain(group: appGroup)
do { do {
try keychain.set(password: password, for: username, label: Bundle.main.bundleIdentifier) try keychain.set(password: password, for: username, context: bundleIdentifier)
} catch _ { } catch _ {
throw ProviderConfigurationError.credentials(details: "keychain.set()") throw ProviderConfigurationError.credentials(details: "keychain.set()")
} }
protocolConfiguration.username = username protocolConfiguration.username = username
protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username) protocolConfiguration.passwordReference = try? keychain.passwordReference(for: username, context: bundleIdentifier)
} }
protocolConfiguration.providerConfiguration = generatedProviderConfiguration(appGroup: appGroup) protocolConfiguration.providerConfiguration = generatedProviderConfiguration(appGroup: appGroup)