0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift KeyChain 保存、更新、取得、削除をする方法

Last updated at Posted at 2026-02-05

実装コード

import Foundation

struct KeyChainManager {
    private init() {}
    
    /// KeyChainに保存する
    /// - Parameters:
    ///  - itemQuery: 保存・更新するデータを示す属性
    ///  - data: 保存・更新するデータ
    /// - Returns: 成功: true, 失敗: false
    private static func save(itemQuery: [String: Any], data: Data) -> Bool {
        let updateQuery = [kSecValueData as String: data] as CFDictionary
        let updateStatus = SecItemUpdate(itemQuery as CFDictionary, updateQuery)
        
        switch updateStatus {
        case errSecSuccess:
            print("⭕️ 更新に成功")
            return true
        case errSecItemNotFound:
            var addQuery = itemQuery
            addQuery[kSecValueData as String] = data
            let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
            if addStatus == errSecSuccess {
                print("⭕️ 保存に成功")
                return true
            }
            print("❌ 保存に失敗: \(errorMessage(for: addStatus))")
            return false
        default:
            print("❌ 更新に失敗: \(errorMessage(for: updateStatus))")
            return false
        }
    }
    
    /// KeyChainから取得する
    /// - Parameter query: 取得するデータを示す属性
    /// - Returns: 取得したデータを文字列化した値
    private static func get(query: [String: Any]) -> String? {
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        if status == errSecSuccess {
            guard let data = item as? Data else { return nil }
            print("⭕️ 取得に成功")
            return String(data: data, encoding: .utf8)
        } else if status == errSecItemNotFound {
            print("ℹ️ 取得データなし: \(errorMessage(for: status))")
            return nil
        }
        
        print("❌ 取得に失敗: \(errorMessage(for: status))")
        return nil
    }
    
    /// KeyChainから削除する
    /// - Parameter query: 削除するデータを示す属性
    /// - Returns: 成功: true, 失敗: false
    private static func delete(query: [String: Any]) -> Bool {
        let status = SecItemDelete(query as CFDictionary)
        if status == errSecSuccess {
            print("⭕️ 削除に成功")
            return true
        } else if status == errSecItemNotFound {
            print("ℹ️ 削除対象のデータなし: \(errorMessage(for: status))")
            return true
        }
        
        print("❌ 削除に失敗: \(errorMessage(for: status))")
        return false
    }
    
    /// OSStatusを出力しても「-25300」このような値しか確認できないため、人が読めるメッセージに変換する
    private static func errorMessage(for status: OSStatus) -> String {
        SecCopyErrorMessageString(status, nil) as? String ?? ""
    }
    
    // MARK: - ログインパスワード
    
    private static let loginPasswordAccount = "login.password"
    
    static func saveLoginPassword(password: String) -> Bool {
        guard
            let data = password.data(using: .utf8),
            let bundleId = Bundle.main.bundleIdentifier
        else { return false }
        
        let query: [String: Any] = [
            // 保存データの種類: 汎用パスワード
            kSecClass as String: kSecClassGenericPassword,
            // おそらく「サービス×アカウント」でデータの識別をする (kSecAttrServerの場合はこれだけで識別してるかも)
            kSecAttrService as String: bundleId,
            kSecAttrAccount as String: loginPasswordAccount,
        ]
        
        return save(itemQuery: query, data: data)
    }
    
    static func getLoginPassword() -> String? {
        guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: bundleId,
            kSecAttrAccount as String: loginPasswordAccount,
            // 取得件数: 1件
            kSecMatchLimit as String: kSecMatchLimitOne,
            // データを返すかどうか (設定しないとデータを取得できない)
            kSecReturnData as String: true
        ]
        
        return get(query: query)
    }
    
    static func deleteLoginPassword() -> Bool {
        guard let bundleId = Bundle.main.bundleIdentifier else { return false }
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: bundleId,
            kSecAttrAccount as String: loginPasswordAccount
        ]
        
        return delete(query: query)
    }
}

備考

KeyChainはiCloudへの同期ができる特徴がありますが、同期したくないと言った場合の方法についても記載がありましたので載せておきます。

参照

クラスkSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyの動作はkSecAttrAccessibleWhenUnlockedと同じですが、利用できるのはデバイスにパスコードが構成されているときのみです。このクラスはシステムキーバッグにのみ存在し、以下の特徴があります。
iCloudキーチェーンに同期されない
バックアップされない
エスクローキーバッグに含まれない
パスコードが削除またはリセットされた場合、クラスキーが破棄されることによって、これらの項目は使用できなくなります。

おそらく実装コードの updateQueryaddQuery に下記のkey: valueを追加すれば良いかと思います。

kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly

まとめ

調べながら実装したので理解を深めるための復習とメモの目的で書きました。
誰かの役に立てれば嬉しいです。
説明に誤りなどがあればご教授頂けますと幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?