Keychain Swift
ライブラリ
Keychain Swift
概要
Keychain SwiftはiOS、macOS、watchOS、tvOS向けのSwiftキーチェーンライブラリで、機密データの安全な保存と取得を簡素化します。
詳細
KeychainAccessやkeychain-swiftなど、複数の優秀なSwiftキーチェーンライブラリが存在します。これらのライブラリは、AppleのKeychainサービスへの簡単なアクセスを提供し、パスワード、クレジットカード番号、秘密トークンなどの機密情報を安全に保存できます。KeychainAccessは最も人気があり、iOS、watchOS、tvOS、macOSの全プラットフォームをサポートし、NSUserDefaultsのような簡単なAPIを提供します。keychain-swiftは軽量で、文字列、ブール値、Dataオブジェクトの保存・取得・削除機能を持ちます。Auth0のSimpleKeychainやJamfのHaversackなど、企業レベルの選択肢も利用可能です。これらのライブラリは、アプリ間でのデータ共有防止、デバイスロック時の追加保護、生体認証との統合をサポートします。
メリット・デメリット
メリット
- 高度なセキュリティ: Appleの暗号化技術による機密データ保護
- クロスプラットフォーム: iOS、macOS、watchOS、tvOS対応
- 簡単なAPI: NSUserDefaultsライクな直感的インターフェース
- 生体認証統合: Touch ID、Face ID、Apple Watchとの連携
- アプリ間分離: 他のアプリからのデータアクセス防止
- 永続化: アプリ削除後もデータが残存(設定次第)
デメリット
- Apple プラットフォーム限定: iOS/macOS以外では使用不可
- 設定複雑性: 高度なセキュリティ設定の学習コスト
- デバッグ困難: シミュレーターでの動作制限
- パフォーマンス: 暗号化処理によるわずかなオーバーヘッド
- エラーハンドリング: Keychainエラーコードのハンドリングが必要
主要リンク
- GitHub - KeychainAccess
- GitHub - keychain-swift
- GitHub - SimpleKeychain (Auth0)
- GitHub - Haversack (Jamf)
- Apple Keychain Services
- Swift公式サイト
書き方の例
KeychainAccess基本使用法
import KeychainAccess
// キーチェーンインスタンスを作成
let keychain = Keychain(service: "com.yourapp.myservice")
// データを保存
keychain["username"] = "john_doe"
keychain["password"] = "secretPassword123"
// データを取得
if let username = keychain["username"] {
print("Username: \(username)")
}
if let password = keychain["password"] {
print("Password: \(password)")
}
// データを削除
keychain["password"] = nil
// または
try keychain.remove("password")
keychain-swift基本使用法
import KeychainSwift
// KeychainSwiftインスタンスを作成
let keychain = KeychainSwift()
// 文字列を保存
keychain.set("john_doe", forKey: "username")
keychain.set("secret123", forKey: "password")
// ブール値を保存
keychain.set(true, forKey: "isLoggedIn")
// データを取得
if let username = keychain.get("username") {
print("Username: \(username)")
}
let isLoggedIn = keychain.getBool("isLoggedIn") ?? false
print("Is logged in: \(isLoggedIn)")
// データを削除
keychain.delete("password")
// 全データをクリア
keychain.clear()
認証トークン管理システム
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) // iCloudキーチェーン同期を無効
.accessibility(.whenUnlockedThisDeviceOnly) // デバイスロック時はアクセス不可
}
// トークンを保存
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)
}
// アクセストークンを取得
func getAccessToken() -> String? {
return keychain[accessTokenKey]
}
// リフレッシュトークンを取得
func getRefreshToken() -> String? {
return keychain[refreshTokenKey]
}
// ユーザーIDを取得
func getUserId() -> String? {
return keychain[userIdKey]
}
// すべてのトークンを削除
func clearTokens() throws {
try keychain.remove(accessTokenKey)
try keychain.remove(refreshTokenKey)
try keychain.remove(userIdKey)
}
// ログイン状態をチェック
func isLoggedIn() -> Bool {
return getAccessToken() != nil && getUserId() != nil
}
}
// 使用例
let authManager = AuthTokenManager()
do {
// ログイン時にトークンを保存
try authManager.saveTokens(
accessToken: "eyJhbGciOiJIUzI1NiIs...",
refreshToken: "refresh_token_value",
userId: "user123"
)
// ログイン状態をチェック
if authManager.isLoggedIn() {
print("User is logged in")
if let token = authManager.getAccessToken() {
// APIリクエストでトークンを使用
print("Using token: \(token)")
}
}
// ログアウト時
try authManager.clearTokens()
} catch {
print("Keychain error: \(error)")
}
生体認証統合
import KeychainAccess
import LocalAuthentication
class BiometricKeychainManager {
private let keychain: Keychain
init() {
keychain = Keychain(service: "com.yourapp.biometric")
.accessibility(.whenPasscodeSetThisDeviceOnly,
authenticationPolicy: .biometryAny)
}
// 生体認証付きでデータを保存
func saveSensitiveData(_ data: String, forKey key: String) throws {
try keychain
.authenticationPrompt("生体認証でデータを保護します")
.set(data, key: key)
}
// 生体認証付きでデータを取得
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("データにアクセスするため生体認証が必要です")
.get(key)
DispatchQueue.main.async {
completion(.success(data))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
// 生体認証の可用性をチェック
func isBiometricAvailable() -> Bool {
let context = LAContext()
var error: NSError?
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
error: &error)
}
// 生体認証のタイプを取得
func biometricType() -> LABiometryType {
let context = LAContext()
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
error: nil)
return context.biometryType
}
}
// 使用例
let biometricManager = BiometricKeychainManager()
// 生体認証が利用可能かチェック
if biometricManager.isBiometricAvailable() {
do {
// 機密データを保存
try biometricManager.saveSensitiveData("secret_api_key",
forKey: "api_key")
// データを取得
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")
}
アプリグループ間でのデータ共有
import KeychainAccess
class SharedKeychainManager {
private let keychain: Keychain
init() {
// App Groupを使用してキーチェーンを共有
keychain = Keychain(service: "com.yourcompany.shared",
accessGroup: "group.com.yourcompany.shared")
.synchronizable(false)
}
// 共有データを保存
func saveSharedData(_ data: String, forKey key: String) throws {
try keychain.set(data, key: key)
}
// 共有データを取得
func getSharedData(forKey key: String) -> String? {
return keychain[key]
}
// 特定のプレフィックスを持つすべてのキーを取得
func getAllKeys(withPrefix prefix: String) -> [String] {
return keychain.allKeys().filter { $0.hasPrefix(prefix) }
}
// データを削除
func removeSharedData(forKey key: String) throws {
try keychain.remove(key)
}
}
// メインアプリとApp Extension間でのデータ共有
let sharedManager = SharedKeychainManager()
do {
// メインアプリでデータを保存
try sharedManager.saveSharedData("shared_secret", forKey: "widget_data")
// App Extensionからデータを取得
if let sharedData = sharedManager.getSharedData(forKey: "widget_data") {
print("Shared data: \(sharedData)")
}
} catch {
print("Shared keychain error: \(error)")
}
設定とユーザープリファレンス管理
import KeychainSwift
class SecureSettingsManager {
private let keychain: KeychainSwift
private let settingsPrefix = "settings_"
init() {
keychain = KeychainSwift()
keychain.synchronizable = false // iCloudキーチェーン同期を無効
}
// セキュアな設定値を保存
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)")
}
}
// セキュアな設定値を取得
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
}
}
// 文字列設定
func setStringValue(_ value: String, forKey key: String) {
keychain.set(value, forKey: settingsPrefix + key)
}
func getStringValue(forKey key: String) -> String? {
return keychain.get(settingsPrefix + key)
}
// ブール設定
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
}
// 設定を削除
func removeSetting(forKey key: String) {
keychain.delete(settingsPrefix + key)
}
// すべての設定をクリア
func clearAllSettings() {
let allKeys = keychain.allKeys.filter { $0.hasPrefix(settingsPrefix) }
for key in allKeys {
keychain.delete(key)
}
}
}
// カスタム設定構造体
struct UserPreferences: Codable {
let theme: String
let notifications: Bool
let language: String
let fontSize: Int
}
// 使用例
let settingsManager = SecureSettingsManager()
// 複雑な設定オブジェクトを保存
let preferences = UserPreferences(
theme: "dark",
notifications: true,
language: "ja",
fontSize: 16
)
settingsManager.setSetting(preferences, forKey: "user_preferences")
// 設定を取得
if let savedPreferences = settingsManager.getSetting(UserPreferences.self,
forKey: "user_preferences") {
print("Theme: \(savedPreferences.theme)")
print("Notifications: \(savedPreferences.notifications)")
}
// 単純な設定値
settingsManager.setStringValue("premium", forKey: "subscription_type")
settingsManager.setBoolValue(true, forKey: "analytics_enabled")
// 設定値を取得
let subscriptionType = settingsManager.getStringValue(forKey: "subscription_type")
let analyticsEnabled = settingsManager.getBoolValue(forKey: "analytics_enabled")
エラーハンドリングとベストプラクティス
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 "キーチェーンアイテムが見つかりません"
case .duplicateItem:
return "アイテムが既に存在します"
case .authenticationFailed:
return "認証に失敗しました"
case .unexpectedData:
return "予期しないデータ形式です"
case .unhandledError(let status):
return "キーチェーンエラー: \(status)"
}
}
}
init() {
keychain = Keychain(service: "com.yourapp.secure")
.accessibility(.whenUnlockedThisDeviceOnly)
}
// 安全なデータ保存
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))
}
}
// 安全なデータ取得
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))
}
}
// キーチェーンエラーをマッピング
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)
}
}
// データの存在確認
func dataExists(forKey key: String) -> Bool {
return keychain[key] != nil
}
// 安全な削除
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))
}
}
}
// 使用例
let secureManager = RobustKeychainManager()
// データを安全に保存
let storeResult = secureManager.securelyStore("sensitive_data", forKey: "secret")
switch storeResult {
case .success:
print("データが正常に保存されました")
case .failure(let error):
print("保存エラー: \(error.localizedDescription)")
}
// データを安全に取得
let retrieveResult = secureManager.securelyRetrieve(forKey: "secret")
switch retrieveResult {
case .success(let data):
if let secretData = data {
print("取得成功: \(secretData)")
} else {
print("データが見つかりません")
}
case .failure(let error):
print("取得エラー: \(error.localizedDescription)")
}