Sources/UberAuth/Token/KeychainUtility.swift (91 lines of code) (raw):
//
// KeychainUtility.swift
// UberAuth
//
// Copyright © 2024 Uber Technologies, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// @mockable
public protocol KeychainUtilityProtocol {
/// Saves an object in the on device keychain using the supplied `key`
///
/// - Parameters:
/// - value: The object to save. Must conform to the Codable protocol.
/// - key: A string value used to identify the saved object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: A boolean indicating whether or not the save operation was successful
func save<V: Encodable>(_ value: V, for key: String, accessGroup: String?) -> Bool
/// Retrieves an object from the on device keychain using the supplied `key`
///
/// - Parameters:
/// - key: The identifier string used when saving the object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: If found, an optional type conforming to the Codable protocol
func get<V: Decodable>(key: String, accessGroup: String?) -> V?
/// Removes the object from the on device keychain corresponding to the supplied `key`
///
/// - Parameters:
/// - key: The identifier string used when saving the object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: A boolean indicating whether or not the delete operation was successful
func delete(key: String, accessGroup: String?) -> Bool
}
public extension KeychainUtilityProtocol {
func save<V: Encodable>(_ value: V, for key: String) -> Bool {
save(value, for: key, accessGroup: nil)
}
func get<V: Decodable>(key: String) -> V? {
get(key: key, accessGroup: nil)
}
func delete(key: String) -> Bool {
delete(key: key, accessGroup: nil)
}
}
public final class KeychainUtility: KeychainUtilityProtocol {
// MARK: Properties
private let serviceName = "com.uber.uber-ios-sdk"
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
// MARK: Initializers
public init() {}
// MARK: KeychainUtilityProtocol
// MARK: Save
/// Saves an object in the on device keychain using the supplied `key`
///
/// - Parameters:
/// - value: The object to save. Must conform to the Codable protocol.
/// - key: A string value used to identify the saved object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: A boolean indicating whether or not the save operation was successful
public func save<V: Encodable>(_ value: V, for key: String, accessGroup: String? = nil) -> Bool {
guard let data = try? encoder.encode(value) else {
return false
}
let valueData = NSData(data: data)
var attributes = attributes(for: key, accessGroup: accessGroup)
attributes[Attribute.accessible] = kSecAttrAccessibleWhenUnlocked
attributes[Attribute.valueData] = valueData
var result: OSStatus = SecItemAdd(
attributes as CFDictionary,
nil
)
if result == errSecDuplicateItem {
result = SecItemUpdate(
attributes as CFDictionary,
[Attribute.valueData: valueData] as CFDictionary
)
}
return result == errSecSuccess
}
// MARK: Get
/// Retrieves an object from the on device keychain using the supplied `key`
///
/// - Parameters:
/// - key: The identifier string used when saving the object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: If found, an optional type conforming to the Codable protocol
public func get<V: Decodable>(key: String, accessGroup: String? = nil) -> V? {
var attributes = attributes(for: key, accessGroup: accessGroup)
attributes[Attribute.matchLimit] = kSecMatchLimitOne
attributes[Attribute.returnData] = kCFBooleanTrue
var obj: AnyObject?
let result = SecItemCopyMatching(
attributes as CFDictionary,
UnsafeMutablePointer(&obj)
)
guard result == noErr else {
return nil
}
guard let data = obj as? Data,
let value = try? decoder.decode(V.self, from: data) else {
return nil
}
return value
}
// MARK: Delete
/// Removes the object from the on device keychain corresponding to the supplied `key`
///
/// - Parameters:
/// - key: The identifier string used when saving the object
/// - accessGroup: The accessGroup for which the operation should be performed
/// - Returns: A boolean indicating whether or not the delete operation was successful
public func delete(key: String, accessGroup: String? = nil) -> Bool {
SecItemDelete(
attributes(for: key, accessGroup: accessGroup) as CFDictionary
) == noErr
}
// MARK: Private
/// Builds a base set of attributes used to perform a keychain storage operation
///
/// - Parameters:
/// - key: The object identifier
/// - accessGroup: An optional access group identifier
/// - Returns: A dictionary containing the attributes
private func attributes(for key: String, accessGroup: String?) -> [String: Any] {
let identifier = key.data(using: .utf8)
var itemData = [String: Any]()
itemData[Attribute.generic] = identifier as AnyObject
itemData[Attribute.account] = identifier as AnyObject
itemData[Attribute.service] = serviceName as AnyObject
itemData[Attribute.class] = kSecClassGenericPassword
if let accessGroup,
!accessGroup.isEmpty {
itemData[Attribute.accessGroup] = accessGroup as AnyObject
}
return itemData
}
// MARK: Constants
enum Attribute {
static let `class` = kSecClass as String
static let account = kSecAttrAccount as String
static let service = kSecAttrService as String
static let accessControl = kSecAttrAccessControl as String
static let accessGroup = kSecAttrAccessGroup as String
static let generic = kSecAttrGeneric as String
static let accessible = kSecAttrAccessible as String
static let returnData = kSecReturnData as String
static let valueData = kSecValueData as String
static let matchLimit = kSecMatchLimit as String
}
}