144 lines
5.0 KiB
Swift
144 lines
5.0 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
/// Actor that handles all Keychain operations in isolation.
|
|
/// Provides thread-safe access to the iOS/watchOS Keychain.
|
|
actor KeychainHelper {
|
|
|
|
public static let shared = KeychainHelper()
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Public Interface
|
|
|
|
/// Stores data in the keychain.
|
|
/// - Parameters:
|
|
/// - data: The data to store.
|
|
/// - service: The service identifier (usually app bundle ID or feature name).
|
|
/// - key: The account/key name.
|
|
/// - accessibility: When the keychain item should be accessible.
|
|
/// - accessControl: Optional access control (biometric, passcode, etc.).
|
|
/// - Throws: `StorageError.keychainError` if the operation fails.
|
|
public func set(
|
|
_ data: Data,
|
|
service: String,
|
|
key: String,
|
|
accessibility: KeychainAccessibility,
|
|
accessControl: KeychainAccessControl? = nil
|
|
) throws {
|
|
var attributes: [String: Any] = [
|
|
kSecValueData as String: data
|
|
]
|
|
|
|
if let accessControl {
|
|
guard let accessControlValue = accessControl.accessControl(accessibility: accessibility) else {
|
|
throw StorageError.securityApplicationFailed
|
|
}
|
|
attributes[kSecAttrAccessControl as String] = accessControlValue
|
|
} else {
|
|
attributes[kSecAttrAccessible as String] = accessibility.cfString
|
|
}
|
|
|
|
let query = baseQuery(service: service, key: key)
|
|
let addQuery = query.merging(attributes) { _, new in new }
|
|
|
|
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
|
|
|
if status == errSecDuplicateItem {
|
|
// Item exists, update it
|
|
let updateStatus = SecItemUpdate(
|
|
query as CFDictionary,
|
|
[kSecValueData as String: data] as CFDictionary
|
|
)
|
|
if updateStatus != errSecSuccess {
|
|
throw StorageError.keychainError(updateStatus)
|
|
}
|
|
} else if status != errSecSuccess {
|
|
throw StorageError.keychainError(status)
|
|
}
|
|
}
|
|
|
|
/// Retrieves data from the keychain.
|
|
/// - Parameters:
|
|
/// - service: The service identifier.
|
|
/// - key: The account/key name.
|
|
/// - Returns: The stored data, or nil if not found.
|
|
/// - Throws: `StorageError.keychainError` if the operation fails.
|
|
public func get(service: String, key: String) throws -> Data? {
|
|
var query = baseQuery(service: service, key: key)
|
|
query[kSecReturnData as String] = true
|
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
|
|
var item: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
|
|
if status == errSecSuccess {
|
|
return item as? Data
|
|
} else if status == errSecItemNotFound {
|
|
return nil
|
|
} else {
|
|
throw StorageError.keychainError(status)
|
|
}
|
|
}
|
|
|
|
/// Deletes data from the keychain.
|
|
/// - Parameters:
|
|
/// - service: The service identifier.
|
|
/// - key: The account/key name.
|
|
/// - Throws: `StorageError.keychainError` if the operation fails (except for item not found).
|
|
public func delete(service: String, key: String) throws {
|
|
let query = baseQuery(service: service, key: key)
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
|
|
if status != errSecSuccess && status != errSecItemNotFound {
|
|
throw StorageError.keychainError(status)
|
|
}
|
|
}
|
|
|
|
/// Checks if an item exists in the keychain.
|
|
/// - Parameters:
|
|
/// - service: The service identifier.
|
|
/// - key: The account/key name.
|
|
/// - Returns: True if the item exists.
|
|
public func exists(service: String, key: String) throws -> Bool {
|
|
var query = baseQuery(service: service, key: key)
|
|
query[kSecReturnData as String] = false
|
|
|
|
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
|
|
|
if status == errSecSuccess {
|
|
return true
|
|
} else if status == errSecItemNotFound {
|
|
return false
|
|
} else {
|
|
throw StorageError.keychainError(status)
|
|
}
|
|
}
|
|
|
|
/// Deletes all items for a given service.
|
|
/// - Parameter service: The service identifier.
|
|
/// - Throws: `StorageError.keychainError` if the operation fails.
|
|
public func deleteAll(service: String) throws {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service
|
|
]
|
|
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
|
|
if status != errSecSuccess && status != errSecItemNotFound {
|
|
throw StorageError.keychainError(status)
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private func baseQuery(service: String, key: String) -> [String: Any] {
|
|
return [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
}
|
|
}
|