Keychain Swift

認証iOSSwiftセキュリティキーチェーンデータ保護

ライブラリ

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エラーコードのハンドリングが必要

主要リンク

書き方の例

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)")
}