import Foundation
import Security
enum KeychainError: Error {
case duplicateItem
case itemNotFound
case invalidData
case unexpectedStatus(OSStatus)
}
class KeychainManager {
static let shared = KeychainManager()
private init() {}
func save(_ data: Data, service: String, account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
try update(data, service: service, account: account)
} else if status != errSecSuccess {
throw KeychainError.unexpectedStatus(status)
}
}
func save(_ string: String, service: String, account: String) throws {
guard let data = string.data(using: .utf8) else {
throw KeychainError.invalidData
}
try save(data, service: service, account: account)
}
func retrieve(service: String, account: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
guard let data = result as? Data else {
throw KeychainError.invalidData
}
return data
}
func retrieveString(service: String, account: String) throws -> String {
let data = try retrieve(service: service, account: account)
guard let string = String(data: data, encoding: .utf8) else {
throw KeychainError.invalidData
}
return string
}
func delete(service: String, account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
private func update(_ data: Data, service: String, account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let attributes: [String: Any] = [
kSecValueData as String: data
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
}
// Convenience extension for auth tokens
extension KeychainManager {
private enum Keys {
static let authToken = "authToken"
static let refreshToken = "refreshToken"
static let service = "com.myapp.auth"
}
func saveAuthToken(_ token: String) throws {
try save(token, service: Keys.service, account: Keys.authToken)
}
func getAuthToken() -> String? {
try? retrieveString(service: Keys.service, account: Keys.authToken)
}
func deleteAuthToken() {
try? delete(service: Keys.service, account: Keys.authToken)
}
}