---
title: Lưu token và session credential trong Keychain - không dùng UserDefaults
impact: CRITICAL
impactDescription: UserDefaults và NSFileManager lưu plaintext trong app sandbox, có thể bị đọc trên jailbroken device, iTunes backup không mã hóa, hoặc ADB pull. Keychain mã hóa bằng Secure Enclave.
tags: swift, ios, keychain, userdefaults, session-management, token-storage, security
---

## Lưu token và session credential trong Keychain - không dùng UserDefaults

Access token, refresh token, user password, và session identifier phải được lưu trong **iOS Keychain** với accessibility phù hợp (`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`). Không dùng `UserDefaults`, `NSFileManager`, hay bất kỳ file plaintext nào.

**Incorrect (lưu token trong UserDefaults):**

```swift
import Foundation

class AuthManager {
    // !! UserDefaults - plaintext, backup leak, jailbreak accessible
    func saveAccessToken(_ token: String) {
        UserDefaults.standard.set(token, forKey: "access_token")
    }

    func getAccessToken() -> String? {
        return UserDefaults.standard.string(forKey: "access_token")
    }

    // !! NSFileManager - file dễ bị đọc qua iTunes backup
    func saveRefreshToken(_ token: String) {
        let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("refresh.token")
        try? token.write(to: path, atomically: true, encoding: .utf8)
    }
}
```

**Correct (Keychain với Secure Enclave):**

```swift
import Foundation
import Security

enum KeychainError: Error {
    case duplicateEntry
    case unknown(OSStatus)
    case noData
    case unexpectedData
}

struct KeychainService {
    static let service = Bundle.main.bundleIdentifier ?? "com.app"

    // SAFE: Lưu token vào Keychain
    static func saveToken(_ token: String, key: String) throws {
        guard let data = token.data(using: .utf8) else { return }

        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: key,
            kSecValueData: data,
            // Chỉ accessible khi device unlocked, không sync iCloud, không backup
            kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]

        // Nếu đã tồn tại, update
        let status = SecItemAdd(query as CFDictionary, nil)
        if status == errSecDuplicateItem {
            let updateQuery: [CFString: Any] = [
                kSecClass: kSecClassGenericPassword,
                kSecAttrService: service,
                kSecAttrAccount: key
            ]
            let updateAttributes: [CFString: Any] = [kSecValueData: data]
            let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
            guard updateStatus == errSecSuccess else {
                throw KeychainError.unknown(updateStatus)
            }
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status)
        }
    }

    // SAFE: Đọc token từ Keychain
    static func readToken(key: String) throws -> String {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: key,
            kSecReturnData: true,
            kSecMatchLimit: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess,
              let data = result as? Data,
              let token = String(data: data, encoding: .utf8) else {
            throw KeychainError.noData
        }
        return token
    }

    // SAFE: Xóa token khi logout
    static func deleteToken(key: String) {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: key
        ]
        SecItemDelete(query as CFDictionary)
    }
}

// Usage
class AuthManager {
    private let accessTokenKey = "com.app.accessToken"
    private let refreshTokenKey = "com.app.refreshToken"

    func saveTokens(accessToken: String, refreshToken: String) throws {
        try KeychainService.saveToken(accessToken, key: accessTokenKey)
        try KeychainService.saveToken(refreshToken, key: refreshTokenKey)
    }

    func clearSession() {
        KeychainService.deleteToken(key: accessTokenKey)
        KeychainService.deleteToken(key: refreshTokenKey)
    }
}
```

**Tools:** Keychain-Swift (library), OWASP MASVS-STORAGE-1, iMazing (backup inspection), objection (jailbreak inspection)
