SwiftのUserDefaultsへの平文保存

SwiftのUserDefaultsへの機密情報の平文保存

説明

UserDefaults は、アプリの設定や小さなユーザー設定値を保存するためのAPIです。認証トークン、セッションID、APIキー、パスワードなどを UserDefaults や NSUbiquitousKeyValueStore に平文で保存すると、端末のバックアップ、デバッグ時の抽出、脱獄した端末、iCloudのキーバリュー同期などを通じて漏えいするおそれがあります。

想定される影響

  • 認証トークンの窃取やセッションの乗っ取り
  • APIキーやクライアントシークレットの漏えい
  • iCloud同期やバックアップを通じた機密情報の拡散
  • 盗まれた値の再利用によるアカウントへのアクセス

対処方法

  1. 可能な限り、機密性のある値を端末に保存しないでください。
  2. 保存が必要な場合は、Keychain Servicesや、鍵を別途管理する検証済みの暗号化ストレージを使います。Secure Enclaveは対応する暗号鍵を保護するものであり、任意の秘密情報を保存する場所ではありません。
  3. UserDefaults には、テーマ、言語、初期設定の完了状況など、機密性のない設定だけを保存します。
  4. 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に、その端末だけで利用でき、パスコードを必要とするアクセス条件で保存します。追加結果を確認し、既存の項目なら値とアクセス条件を明示的に更新します。ほかのエラーでは弱い保存先に切り替えず、呼び出し元に失敗を伝えます。

参考資料