Keychain Swift
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
- GitHub - KeychainAccess
- GitHub - keychain-swift
- GitHub - SimpleKeychain (Auth0)
- GitHub - Haversack (Jamf)
- Apple Keychain Services
- Swift Official Site
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)")
}