実装コード
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キーチェーンに同期されない
バックアップされない
エスクローキーバッグに含まれない
パスコードが削除またはリセットされた場合、クラスキーが破棄されることによって、これらの項目は使用できなくなります。
おそらく実装コードの updateQuery と addQuery に下記のkey: valueを追加すれば良いかと思います。
kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
まとめ
調べながら実装したので理解を深めるための復習とメモの目的で書きました。
誰かの役に立てれば嬉しいです。
説明に誤りなどがあればご教授頂けますと幸いです。