説明
UserDefaults は、アプリの設定や小さなユーザー設定値を保存するためのAPIです。認証トークン、セッションID、APIキー、パスワードなどを UserDefaults や NSUbiquitousKeyValueStore に平文で保存すると、端末のバックアップ、デバッグ時の抽出、脱獄した端末、iCloudのキーバリュー同期などを通じて漏えいするおそれがあります。
想定される影響
- 認証トークンの窃取やセッションの乗っ取り
- APIキーやクライアントシークレットの漏えい
- iCloud同期やバックアップを通じた機密情報の拡散
- 盗まれた値の再利用によるアカウントへのアクセス
対処方法
- 可能な限り、機密性のある値を端末に保存しないでください。
- 保存が必要な場合は、Keychain Servicesや、鍵を別途管理する検証済みの暗号化ストレージを使います。Secure Enclaveは対応する暗号鍵を保護するものであり、任意の秘密情報を保存する場所ではありません。
UserDefaultsには、テーマ、言語、初期設定の完了状況など、機密性のない設定だけを保存します。NSUbiquitousKeyValueStoreには、iCloud同期が必要な、機密性のない設定だけを保存します。
例
変更前
swift
func saveToken(token: String) {
UserDefaults.standard.set(token, forKey: "access_token")
}
変更後
swift
import Foundation
import Security
enum KeychainStorageError: Error {
case unexpectedStatus(OSStatus)
}
func saveToken(token: Data) throws {
let item: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app.auth",
kSecAttrAccount as String: "access_token",
kSecAttrAccessible as String:
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecValueData as String: token
]
let addStatus = SecItemAdd(item as CFDictionary, nil)
if addStatus == errSecSuccess { return }
guard addStatus == errSecDuplicateItem else {
throw KeychainStorageError.unexpectedStatus(addStatus)
}
let match: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app.auth",
kSecAttrAccount as String: "access_token"
]
let updates: [String: Any] = [
kSecAttrAccessible as String:
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecValueData as String: token
]
let updateStatus = SecItemUpdate(
match as CFDictionary, updates as CFDictionary)
guard updateStatus == errSecSuccess else {
throw KeychainStorageError.unexpectedStatus(updateStatus)
}
}
説明:
- 変更前:
UserDefaultsに平文で保存した認証トークンは、アプリのサンドボックスからの抽出、バックアップ、デバッグ環境を通じて漏えいするおそれがあります。 - 変更後: Keychain Servicesに、その端末だけで利用でき、パスコードを必要とするアクセス条件で保存します。追加結果を確認し、既存の項目なら値とアクセス条件を明示的に更新します。ほかのエラーでは弱い保存先に切り替えず、呼び出し元に失敗を伝えます。