Keychain wrapper for secure credential storage

Sofia Martinez Jan 2026
1 tab
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)
    }
}
1 file · swift Explain with highlit

The Keychain securely stores sensitive data like passwords and tokens, encrypting them at the OS level. Direct Keychain APIs are verbose, so I create a wrapper class that simplifies common operations. The wrapper uses Security framework's SecItemAdd, SecItemCopyMatching, and SecItemDelete functions. I store strings by encoding to Data and specify kSecClassGenericPassword for credentials. Query dictionaries identify items by service and account names. The Keychain persists across app installs and backs up to iCloud Keychain. For access control, I set kSecAttrAccessible to restrict when data is available. This approach is far more secure than UserDefaults for authentication tokens.