コード
使用するには型を合わせる必要があります。
バージョン1
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import Foundation import Security final class KeychainHelper { // MARK: - 保存(Dataを暗号化して安全に保存) static func save(_ data: Data, service: String, account: String) { // 既存データがあれば削除 delete(service: service, account: account) // 登録クエリ let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecValueData as String: data ] SecItemAdd(query as CFDictionary, nil) } // MARK: - 読み込み(Dataを返す) static func load(service: String, account: String) -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess else { return nil } return item as? Data } // MARK: - 削除(ログアウトやトークン再発行時に使用) static func delete(service: String, account: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account ] SecItemDelete(query as CFDictionary) } // MARK: - 存在チェック(Keychainにデータがあるか) static func exists(service: String, account: String) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: false, kSecMatchLimit as String: kSecMatchLimitOne ] let status = SecItemCopyMatching(query as CFDictionary, nil) return status == errSecSuccess } // MARK: - すべて削除(開発・デバッグ用) static func clearAll() { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword ] SecItemDelete(query as CFDictionary) } } |
バージョン2
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
import Foundation import Security final class KeychainHelper { // 保存 static func save(service: String, account: String, value: String) throws { let data = Data(value.utf8) // 既存のデータがあれば削除 try? delete(service: service, account: account) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecValueData as String: data ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } } // 読み込み static func read(service: String, account: String) throws -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess else { if status == errSecItemNotFound { return nil } throw KeychainError.unhandledError(status: status) } guard let data = item as? Data, let value = String(data: data, encoding: .utf8) else { throw KeychainError.invalidData } return value } // 削除 static func delete(service: String, account: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } } // エラー定義 enum KeychainError: Error { case invalidData case unhandledError(status: OSStatus) } } |
✅ 使い方
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let service = "com.weatherapp" let account = "authToken" do { // 保存 try KeychainHelper.save(service: service, account: account, value: "my-secret-token-123") // 読み込み if let token = try KeychainHelper.read(service: service, account: account) { print("取得したトークン:", token) } // 削除 try KeychainHelper.delete(service: service, account: account) print("トークン削除済み") } catch { print("Keychain エラー:", error) } |
| メソッド | 用途 | 使用例 |
|---|---|---|
save(data:service:account:) | トークンを保存 | KeychainHelper.save(data, service: "com.example.app", account: "token") |
load(service:account:) | トークンを読み出す | KeychainHelper.load(service: "com.example.app", account: "token") |
delete(service:account:) | 特定のトークンを削除 | KeychainHelper.delete(service: "com.example.app", account: "token") |
exists(service:account:) | トークンがあるか確認 | if KeychainHelper.exists(service: "com.example.app", account: "token") { ... } |
clearAll() | 全削除(開発用・慎重に) | KeychainHelper.clearAll() |
✅ 実用例(ログアウト時や再取得時)
|
1 2 3 4 |
if KeychainHelper.exists(service: "com.example.tsuriba", account: "authToken") { KeychainHelper.delete(service: "com.example.tsuriba", account: "authToken") print("トークンを削除しました。次回アクセス時に再取得します。") } |
🔹Keychain の「安全な上書き(更新)」方法
Keychain には本来「上書き」というメソッドはありません。
代わりに Apple が推奨しているのは SecItemUpdate を使う方法です。
KeychainHelper にこの更新メソッドを追加する場合は、こうなります👇
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
static func update(_ data: Data, service: String, account: String) -> Bool { // 更新対象を指定するクエリ let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account ] // 更新内容 let attributes: [String: Any] = [ kSecValueData as String: data ] // 既存があれば更新、なければ新規作成 let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) if status == errSecItemNotFound { // 無ければ保存 save(data, service: service, account: account) return true } return status == errSecSuccess } |
service と account について
Keychain の保存は「service」と「account」という2つのキーで識別されます。
- service
- account
つまり、同じ service 内に複数の account を持てる仕組みになっています。
👉 任意の文字列でOKですが、「後から見てわかりやすい」命名にすると管理が楽です。
命名のベストプラクティス
service→ アプリ固有の文字列(com.gacharis.weatherapp)account→ 保存するデータの種類(authToken/refreshToken)
これで一目見て「どのアプリの何のデータか」がわかるようになります。
CFDictionary
CFDictionary とは?
- Core Foundation フレームワークの 辞書型(連想配列)のことです。
- Swift や Objective-C から使うときは、通常の
Dictionary<String, Any>をas CFDictionaryでキャストして渡せば使えます。 - なぜ必要かというと、Keychain の低レベル API (
SecItemAdd,SecItemCopyMatching, etc.) は Core Foundation 時代の C API をベースに作られているからです。
例で分解すると
|
1 2 3 4 5 6 7 8 |
let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecValueData as String: data ] let status = SecItemAdd(query as CFDictionary, nil) |
ここでやっていることは:
- Swift の辞書
queryを作る SecItemAddは C の関数なので Swift のDictionaryをそのまま渡せない
→ そこでas CFDictionaryをつけて「Core Foundation 辞書」に変換している。
参考:SecItemAdd のシグネチャ
|
1 |
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus |
- func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer?) -> OSStatus
- func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer?) -> OSStatus
- 第二引数:
UnsafeMutablePointer<CFTypeRef?>?(結果を受け取るポインタ、今回は不要なのでnil)
**CFDictionary は「Keychain API が要求する辞書の型」**で、Swift の辞書をそのままキャストして渡しているだけ。
コメント