[Xcode/SwiftUI] KeychainでIDとPasswordを保存する

UserDefaultsだとアプリを消すとデータもリセットされるが、Keychainを使うとアプリを消しても残り続けるので便利という感じ。

実装

KeychainHelper.swift

シングルトンのKeychain用のインスタンスを準備する、ここで保存、取得、削除を担わせるようにする。

import Security
import SwiftUI

final class KeychainHelper {
    static let shared = KeychainHelper()
    private init() {}

    // 保存
    func save(_ data: Data, forKey key: String) -> Bool {
        // Keychainに保存するためのクエリ作成
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword, // データの種類を指定(ここでは一般的なパスワードとして保存)
            kSecAttrAccount as String: key, // 保存するデータに関連するキーを指定
            kSecValueData as String: data // 保存するデータ自体
        ]
        // 既存のデータがある場合は削除する(同じキーで新しいデータを保存するため)
        SecItemDelete(query as CFDictionary)
        // Keychainにデータを保存 (成功すればtrueを返す)
        return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
    }

    // 読み込み
    func read(forKey key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key, // 読み込み対象のキーを指定
            kSecReturnData as String: true, // データを返すように指定
            kSecMatchLimit as String: kSecMatchLimitOne // 最初の一致するデータのみ取得
        ]

        var dataTypeRef: AnyObject? // 取得したデータを格納するための変数
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) // Keychainからデータを検索
        if status == errSecSuccess {
            return dataTypeRef as? Data // 取得したデータを返す
        }
        // データが見つからなかった場合はnilを返す
        return nil
    }

    // 削除
    func delete(forKey key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]

        return SecItemDelete(query as CFDictionary) == errSecSuccess
    }
}

Homeview.swift

import SwiftUI

struct HomeView: View {
    @State private var userId: String = ""
    @State private var password: String = ""
    @State private var retrievedId: String = ""
    @State private var retrievedPassword: String = ""
    @State private var message: String = ""

    private let idKey = "userID"
    private let passwordKey = "userPassword"

    var body: some View {
        VStack(spacing: 8) {
            Text("Keychain Example")
                .font(.largeTitle)
                .bold()

            TextField("Enter User ID", text: $userId)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            SecureField("Enter Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            Button("Save to Keychain") {
                saveToKeychain()
            }
            .buttonStyle(.borderedProminent)

            Button("Retrieve from Keychain") {
                retrieveFromKeychain()
            }
            .buttonStyle(.bordered)

            Button("Delete from Keychain") {
                deleteFromKeychain()
            }
            .buttonStyle(.bordered)

            Text("Retrieved ID: \(retrievedId)")
            Text("Retrieved Password: \(retrievedPassword)")
            Text("Message: \(message)")
                .foregroundColor(.red)
        }
        .padding()
    }
}

private extension HomeView {
    func saveToKeychain() {
        guard let idData = userId.data(using: .utf8), let passwordData = password.data(using: .utf8) else {
            message = "Failed to encode data"
            return
        }
        let idSaved = KeychainHelper.shared.save(idData, forKey: idKey)
        let passwordSaved = KeychainHelper.shared.save(passwordData, forKey: passwordKey)
        message = (idSaved && passwordSaved) ? "Saved successfully" : "Failed to save"
    }

    func retrieveFromKeychain() {
        if let idData = KeychainHelper.shared.read(forKey: idKey), let passwordData = KeychainHelper.shared.read(forKey: passwordKey) {
            retrievedId = String(data: idData, encoding: .utf8) ?? "N/A"
            retrievedPassword = String(data: passwordData, encoding: .utf8) ?? "N/A"
            message = "Retrieved successfully"
        } else {
            message = "No data found"
        }
    }

    func deleteFromKeychain() {
        let idDeleted = KeychainHelper.shared.delete(forKey: idKey)
        let passwordDeleted = KeychainHelper.shared.delete(forKey: passwordKey)

        if idDeleted && passwordDeleted {
            retrievedId = ""
            retrievedPassword = ""
            message = "Deleted successfully!"
        } else {
            message = "Failed to delete data"
        }
    }
}

Keychainのメリット

  1. セキュリティが高い
    • Keychainはデータを暗号化して保存するため、セキュリティが高い。
    • 保存されたデータはOSのセキュリティフレームワークに保護され、他のアプリやプロセスからアクセスできない。
  2. iCloud同期
    • iCloudと連携して、同じApple IDを使用している他のデバイスともデータを同期することができる。
  3. システム統合
    • Appleのセキュリティフレームワークと統合されており、ユーザーの認証情報(例:パスワードやセキュリティトークン)の管理が簡単になる。
  4. 自動でデータの暗号化と復号化
    • 開発者はKeychainにデータを保存する際に暗号化の設定を行う必要がなく、Keychainが自動でデータを暗号化および復号化する。
  5. 簡単に利用できるAPI
    • KeychainのAPIは使いやすく、データの保存、読み込み、削除を簡単に行うことができる。

Keychainのデメリット

  1. 容量制限
    • Keychainには保存できるデータの容量に制限があり、大量のデータを保存することはできない。通常はパスワードや認証情報など、少量のデータ向け。
  2. データの同期には時間がかかる場合がある
    • iCloud同期機能を使用している場合、デバイス間での同期に時間がかかることがある。
  3. 読み書きのパフォーマンスに影響
    • Keychainは暗号化されたストレージを提供するため、大量のデータを頻繁に読み書きする場合、パフォーマンスが低下することがある。
  4. iOS/macOS専用
    • KeychainはAppleのエコシステム内でのみ利用可能であり、Androidや他のプラットフォームでの利用ができない。クロスプラットフォームでデータを扱う必要がある場合は、他のストレージ方法を検討する必要がある。
  5. Keychainのクリア時の注意点
    • Keychainからデータを削除する際に、すべてのデータが一度に削除される場合があり、重要な情報が消失するリスクがるため、削除操作には注意が必要。