Keychain Swift

AuthenticationiOSSwiftSecurityKeychainData Protection

Library

Keychain Swift

Overview

Keychain Swift is a Swift keychain library for iOS, macOS, watchOS, and tvOS that simplifies secure storage and retrieval of sensitive data.

Details

Several excellent Swift keychain libraries exist, including KeychainAccess and keychain-swift. These libraries provide easy access to Apple's Keychain services, allowing secure storage of sensitive information such as passwords, credit card numbers, and secret tokens. KeychainAccess is the most popular, supporting all platforms (iOS, watchOS, tvOS, macOS) and providing an API similar to NSUserDefaults. keychain-swift is lightweight with functionality for storing, retrieving, and deleting strings, booleans, and Data objects. Enterprise-level options like Auth0's SimpleKeychain and Jamf's Haversack are also available. These libraries support preventing data sharing between apps, additional protection when devices are locked, and integration with biometric authentication.

Pros and Cons

Pros

  • Advanced Security: Sensitive data protection through Apple's encryption technology
  • Cross-Platform: Support for iOS, macOS, watchOS, tvOS
  • Simple API: Intuitive interface similar to NSUserDefaults
  • Biometric Integration: Integration with Touch ID, Face ID, Apple Watch
  • App Isolation: Prevention of data access from other apps
  • Persistence: Data remains after app deletion (depending on settings)

Cons

  • Apple Platform Only: Cannot be used outside iOS/macOS
  • Configuration Complexity: Learning curve for advanced security settings
  • Debugging Difficulty: Limited functionality in simulator
  • Performance: Slight overhead from encryption processing
  • Error Handling: Need to handle keychain error codes

Main Links

Code Examples

KeychainAccess Basic Usage

import KeychainAccess

// Create keychain instance
let keychain = Keychain(service: "com.yourapp.myservice")

// Store data
keychain["username"] = "john_doe"
keychain["password"] = "secretPassword123"

// Retrieve data
if let username = keychain["username"] {
    print("Username: \(username)")
}

if let password = keychain["password"] {
    print("Password: \(password)")
}

// Delete data
keychain["password"] = nil

// Or
try keychain.remove("password")

keychain-swift Basic Usage

import KeychainSwift

// Create KeychainSwift instance
let keychain = KeychainSwift()

// Store string
keychain.set("john_doe", forKey: "username")
keychain.set("secret123", forKey: "password")

// Store boolean
keychain.set(true, forKey: "isLoggedIn")

// Retrieve data
if let username = keychain.get("username") {
    print("Username: \(username)")
}

let isLoggedIn = keychain.getBool("isLoggedIn") ?? false
print("Is logged in: \(isLoggedIn)")

// Delete data
keychain.delete("password")

// Clear all data
keychain.clear()

Authentication Token Management System

import KeychainAccess
import Foundation

class AuthTokenManager {
    private let keychain: Keychain
    private let accessTokenKey = "access_token"
    private let refreshTokenKey = "refresh_token"
    private let userIdKey = "user_id"
    
    init() {
        keychain = Keychain(service: "com.yourapp.auth")
            .synchronizable(false) // Disable iCloud keychain sync
            .accessibility(.whenUnlockedThisDeviceOnly) // No access when device is locked
    }
    
    // Save tokens
    func saveTokens(accessToken: String, refreshToken: String, userId: String) throws {
        try keychain.set(accessToken, key: accessTokenKey)
        try keychain.set(refreshToken, key: refreshTokenKey)
        try keychain.set(userId, key: userIdKey)
    }
    
    // Get access token
    func getAccessToken() -> String? {
        return keychain[accessTokenKey]
    }
    
    // Get refresh token
    func getRefreshToken() -> String? {
        return keychain[refreshTokenKey]
    }
    
    // Get user ID
    func getUserId() -> String? {
        return keychain[userIdKey]
    }
    
    // Clear all tokens
    func clearTokens() throws {
        try keychain.remove(accessTokenKey)
        try keychain.remove(refreshTokenKey)
        try keychain.remove(userIdKey)
    }
    
    // Check login status
    func isLoggedIn() -> Bool {
        return getAccessToken() != nil && getUserId() != nil
    }
}

// Usage example
let authManager = AuthTokenManager()

do {
    // Save tokens on login
    try authManager.saveTokens(
        accessToken: "eyJhbGciOiJIUzI1NiIs...",
        refreshToken: "refresh_token_value",
        userId: "user123"
    )
    
    // Check login status
    if authManager.isLoggedIn() {
        print("User is logged in")
        
        if let token = authManager.getAccessToken() {
            // Use token in API requests
            print("Using token: \(token)")
        }
    }
    
    // On logout
    try authManager.clearTokens()
} catch {
    print("Keychain error: \(error)")
}

Biometric Authentication Integration

import KeychainAccess
import LocalAuthentication

class BiometricKeychainManager {
    private let keychain: Keychain
    
    init() {
        keychain = Keychain(service: "com.yourapp.biometric")
            .accessibility(.whenPasscodeSetThisDeviceOnly, 
                          authenticationPolicy: .biometryAny)
    }
    
    // Save data with biometric authentication
    func saveSensitiveData(_ data: String, forKey key: String) throws {
        try keychain
            .authenticationPrompt("Biometric authentication will protect your data")
            .set(data, key: key)
    }
    
    // Get data with biometric authentication
    func getSensitiveData(forKey key: String, completion: @escaping (Result<String?, Error>) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            do {
                let data = try self?.keychain
                    .authenticationPrompt("Biometric authentication required to access data")
                    .get(key)
                
                DispatchQueue.main.async {
                    completion(.success(data))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
    }
    
    // Check biometric availability
    func isBiometricAvailable() -> Bool {
        let context = LAContext()
        var error: NSError?
        
        return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, 
                                       error: &error)
    }
    
    // Get biometric type
    func biometricType() -> LABiometryType {
        let context = LAContext()
        _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, 
                                    error: nil)
        return context.biometryType
    }
}

// Usage example
let biometricManager = BiometricKeychainManager()

// Check if biometric authentication is available
if biometricManager.isBiometricAvailable() {
    do {
        // Save sensitive data
        try biometricManager.saveSensitiveData("secret_api_key", 
                                             forKey: "api_key")
        
        // Retrieve data
        biometricManager.getSensitiveData(forKey: "api_key") { result in
            switch result {
            case .success(let data):
                if let apiKey = data {
                    print("API Key: \(apiKey)")
                }
            case .failure(let error):
                print("Error: \(error.localizedDescription)")
            }
        }
    } catch {
        print("Save error: \(error)")
    }
} else {
    print("Biometric authentication not available")
}

Data Sharing Between App Groups

import KeychainAccess

class SharedKeychainManager {
    private let keychain: Keychain
    
    init() {
        // Share keychain using App Group
        keychain = Keychain(service: "com.yourcompany.shared", 
                          accessGroup: "group.com.yourcompany.shared")
            .synchronizable(false)
    }
    
    // Save shared data
    func saveSharedData(_ data: String, forKey key: String) throws {
        try keychain.set(data, key: key)
    }
    
    // Get shared data
    func getSharedData(forKey key: String) -> String? {
        return keychain[key]
    }
    
    // Get all keys with specific prefix
    func getAllKeys(withPrefix prefix: String) -> [String] {
        return keychain.allKeys().filter { $0.hasPrefix(prefix) }
    }
    
    // Delete data
    func removeSharedData(forKey key: String) throws {
        try keychain.remove(key)
    }
}

// Data sharing between main app and App Extension
let sharedManager = SharedKeychainManager()

do {
    // Save data from main app
    try sharedManager.saveSharedData("shared_secret", forKey: "widget_data")
    
    // Get data from App Extension
    if let sharedData = sharedManager.getSharedData(forKey: "widget_data") {
        print("Shared data: \(sharedData)")
    }
} catch {
    print("Shared keychain error: \(error)")
}

Settings and User Preferences Management

import KeychainSwift

class SecureSettingsManager {
    private let keychain: KeychainSwift
    private let settingsPrefix = "settings_"
    
    init() {
        keychain = KeychainSwift()
        keychain.synchronizable = false // Disable iCloud keychain sync
    }
    
    // Save secure setting value
    func setSetting<T: Codable>(_ value: T, forKey key: String) {
        do {
            let data = try JSONEncoder().encode(value)
            keychain.set(data, forKey: settingsPrefix + key)
        } catch {
            print("Failed to encode setting: \(error)")
        }
    }
    
    // Get secure setting value
    func getSetting<T: Codable>(_ type: T.Type, forKey key: String) -> T? {
        guard let data = keychain.getData(settingsPrefix + key) else {
            return nil
        }
        
        do {
            return try JSONDecoder().decode(type, from: data)
        } catch {
            print("Failed to decode setting: \(error)")
            return nil
        }
    }
    
    // String settings
    func setStringValue(_ value: String, forKey key: String) {
        keychain.set(value, forKey: settingsPrefix + key)
    }
    
    func getStringValue(forKey key: String) -> String? {
        return keychain.get(settingsPrefix + key)
    }
    
    // Boolean settings
    func setBoolValue(_ value: Bool, forKey key: String) {
        keychain.set(value, forKey: settingsPrefix + key)
    }
    
    func getBoolValue(forKey key: String, defaultValue: Bool = false) -> Bool {
        return keychain.getBool(settingsPrefix + key) ?? defaultValue
    }
    
    // Remove setting
    func removeSetting(forKey key: String) {
        keychain.delete(settingsPrefix + key)
    }
    
    // Clear all settings
    func clearAllSettings() {
        let allKeys = keychain.allKeys.filter { $0.hasPrefix(settingsPrefix) }
        for key in allKeys {
            keychain.delete(key)
        }
    }
}

// Custom settings structure
struct UserPreferences: Codable {
    let theme: String
    let notifications: Bool
    let language: String
    let fontSize: Int
}

// Usage example
let settingsManager = SecureSettingsManager()

// Save complex settings object
let preferences = UserPreferences(
    theme: "dark",
    notifications: true,
    language: "en",
    fontSize: 16
)
settingsManager.setSetting(preferences, forKey: "user_preferences")

// Get settings
if let savedPreferences = settingsManager.getSetting(UserPreferences.self, 
                                                   forKey: "user_preferences") {
    print("Theme: \(savedPreferences.theme)")
    print("Notifications: \(savedPreferences.notifications)")
}

// Simple setting values
settingsManager.setStringValue("premium", forKey: "subscription_type")
settingsManager.setBoolValue(true, forKey: "analytics_enabled")

// Get setting values
let subscriptionType = settingsManager.getStringValue(forKey: "subscription_type")
let analyticsEnabled = settingsManager.getBoolValue(forKey: "analytics_enabled")

Error Handling and Best Practices

import KeychainAccess

class RobustKeychainManager {
    private let keychain: Keychain
    
    enum KeychainError: Error, LocalizedError {
        case itemNotFound
        case duplicateItem
        case authenticationFailed
        case unexpectedData
        case unhandledError(status: OSStatus)
        
        var errorDescription: String? {
            switch self {
            case .itemNotFound:
                return "Keychain item not found"
            case .duplicateItem:
                return "Item already exists"
            case .authenticationFailed:
                return "Authentication failed"
            case .unexpectedData:
                return "Unexpected data format"
            case .unhandledError(let status):
                return "Keychain error: \(status)"
            }
        }
    }
    
    init() {
        keychain = Keychain(service: "com.yourapp.secure")
            .accessibility(.whenUnlockedThisDeviceOnly)
    }
    
    // Secure data storage
    func securelyStore(_ data: String, forKey key: String) -> Result<Void, KeychainError> {
        do {
            try keychain.set(data, key: key)
            return .success(())
        } catch let error as OSStatus {
            return .failure(mapKeychainError(error))
        } catch {
            return .failure(.unhandledError(status: -1))
        }
    }
    
    // Secure data retrieval
    func securelyRetrieve(forKey key: String) -> Result<String?, KeychainError> {
        do {
            let data = try keychain.get(key)
            return .success(data)
        } catch let error as OSStatus {
            return .failure(mapKeychainError(error))
        } catch {
            return .failure(.unhandledError(status: -1))
        }
    }
    
    // Map keychain errors
    private func mapKeychainError(_ status: OSStatus) -> KeychainError {
        switch status {
        case errSecItemNotFound:
            return .itemNotFound
        case errSecDuplicateItem:
            return .duplicateItem
        case errSecAuthFailed:
            return .authenticationFailed
        case errSecNotAvailable:
            return .authenticationFailed
        default:
            return .unhandledError(status: status)
        }
    }
    
    // Check data existence
    func dataExists(forKey key: String) -> Bool {
        return keychain[key] != nil
    }
    
    // Secure deletion
    func securelyDelete(forKey key: String) -> Result<Void, KeychainError> {
        do {
            try keychain.remove(key)
            return .success(())
        } catch let error as OSStatus {
            return .failure(mapKeychainError(error))
        } catch {
            return .failure(.unhandledError(status: -1))
        }
    }
}

// Usage example
let secureManager = RobustKeychainManager()

// Safely store data
let storeResult = secureManager.securelyStore("sensitive_data", forKey: "secret")
switch storeResult {
case .success:
    print("Data stored successfully")
case .failure(let error):
    print("Store error: \(error.localizedDescription)")
}

// Safely retrieve data
let retrieveResult = secureManager.securelyRetrieve(forKey: "secret")
switch retrieveResult {
case .success(let data):
    if let secretData = data {
        print("Retrieved successfully: \(secretData)")
    } else {
        print("Data not found")
    }
case .failure(let error):
    print("Retrieve error: \(error.localizedDescription)")
}