116 lines
3.9 KiB
Swift
116 lines
3.9 KiB
Swift
import Foundation
|
|
import Security
|
|
import os
|
|
|
|
/// Securely stores and retrieves API keys using Keychain with iCloud sync.
|
|
final class KeychainService {
|
|
static let shared = KeychainService()
|
|
|
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "KeychainService")
|
|
private let service = "com.prakashjoshipax.VoiceInk"
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Public API
|
|
|
|
/// Saves a string value to Keychain.
|
|
@discardableResult
|
|
func save(_ value: String, forKey key: String, syncable: Bool = true) -> Bool {
|
|
guard let data = value.data(using: .utf8) else {
|
|
logger.error("Failed to convert value to data for key: \(key)")
|
|
return false
|
|
}
|
|
return save(data: data, forKey: key, syncable: syncable)
|
|
}
|
|
|
|
/// Saves data to Keychain.
|
|
@discardableResult
|
|
func save(data: Data, forKey key: String, syncable: Bool = true) -> Bool {
|
|
// First, try to delete any existing item to avoid duplicates
|
|
delete(forKey: key, syncable: syncable)
|
|
|
|
var query = baseQuery(forKey: key, syncable: syncable)
|
|
query[kSecValueData as String] = data
|
|
|
|
let status = SecItemAdd(query as CFDictionary, nil)
|
|
|
|
if status == errSecSuccess {
|
|
logger.info("Successfully saved keychain item for key: \(key)")
|
|
return true
|
|
} else {
|
|
logger.error("Failed to save keychain item for key: \(key), status: \(status)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Retrieves a string value from Keychain.
|
|
func getString(forKey key: String, syncable: Bool = true) -> String? {
|
|
guard let data = getData(forKey: key, syncable: syncable) else {
|
|
return nil
|
|
}
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
/// Retrieves data from Keychain.
|
|
func getData(forKey key: String, syncable: Bool = true) -> Data? {
|
|
var query = baseQuery(forKey: key, syncable: syncable)
|
|
query[kSecReturnData as String] = kCFBooleanTrue
|
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
|
|
if status == errSecSuccess {
|
|
return result as? Data
|
|
} else if status != errSecItemNotFound {
|
|
logger.error("Failed to retrieve keychain item for key: \(key), status: \(status)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Deletes an item from Keychain.
|
|
@discardableResult
|
|
func delete(forKey key: String, syncable: Bool = true) -> Bool {
|
|
let query = baseQuery(forKey: key, syncable: syncable)
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
|
|
if status == errSecSuccess || status == errSecItemNotFound {
|
|
if status == errSecSuccess {
|
|
logger.info("Successfully deleted keychain item for key: \(key)")
|
|
}
|
|
return true
|
|
} else {
|
|
logger.error("Failed to delete keychain item for key: \(key), status: \(status)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Checks if a key exists in Keychain.
|
|
func exists(forKey key: String, syncable: Bool = true) -> Bool {
|
|
var query = baseQuery(forKey: key, syncable: syncable)
|
|
query[kSecReturnData as String] = kCFBooleanFalse
|
|
|
|
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
|
return status == errSecSuccess
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
/// Creates base Keychain query dictionary.
|
|
private func baseQuery(forKey key: String, syncable: Bool) -> [String: Any] {
|
|
var query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecUseDataProtectionKeychain as String: true
|
|
]
|
|
|
|
if syncable {
|
|
query[kSecAttrSynchronizable as String] = kCFBooleanTrue
|
|
}
|
|
|
|
return query
|
|
}
|
|
}
|