Skip to content
intermediate Phase 6 · Data Persistence

Keychain Services

Securely store tokens, passwords, and sensitive data using the iOS Keychain.

40m
2 problems
Topic Progress 0%

Keychain Basics

What is Keychain?

Keychain Services is Apple's secure storage system for sensitive data like passwords, tokens, and encryption keys. Data stored in Keychain is encrypted and persists across app reinstalls.

When to Use Keychain

Use Keychain for:

  • Authentication tokens and session data
  • API keys and secrets
  • Passwords and credentials
  • Encryption keys
  • Any data that must survive app deletion

Do NOT use Keychain for:

  • User preferences (use UserDefaults)
  • Large data blobs (use file system)
  • Temporary data

Keychain vs UserDefaults

  • UserDefaults: Plaintext, not encrypted, deleted with app
  • Keychain: Encrypted, survives app deletion, access-controlled

Basic Usage with SecItem

The SecItem API is the low-level interface:

import Security

// Add
let data = "secret-token".data(using: .utf8)!
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: "com.app.auth",
    kSecAttrAccount as String: "user-token",
    kSecValueData as String: data,
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)

Access Control

Keychain items have accessibility levels:

  • kSecAttrAccessibleWhenUnlocked: Only when device is unlocked
  • kSecAttrAccessibleAfterFirstUnlock: After first unlock until restart
  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly: Unlocked, not backed up
  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: After first unlock, not backed up

SecItem API

Reading Keychain Items

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 result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)

    guard status == errSecSuccess, let data = result as? Data else {
        return nil
    }
    return data
}

Updating Keychain Items

func save(service: String, account: String, data: Data) {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account
    ]

    let updateQuery: [String: Any] = [
        kSecValueData as String: data
    ]

    let status = SecItemUpdate(query as CFDictionary, updateQuery as CFDictionary)

    if status == errSecItemNotFound {
        var addQuery = query
        addQuery[kSecValueData as String] = data
        SecItemAdd(addQuery as CFDictionary, nil)
    }
}

Deleting Keychain Items

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)
}

Error Handling

let status = SecItemAdd(query as CFDictionary, nil)
switch status {
case errSecSuccess:
    print("Saved successfully")
case errSecDuplicateItem:
    print("Item already exists")
case errSecItemNotFound:
    print("Item not found")
default:
    print("Error: \(status)")
}

Keychain Wrapper

Building a Reusable Wrapper

Simplify keychain access with a wrapper class:

class KeychainWrapper {
    private let service: String

    init(service: String = Bundle.main.bundleIdentifier ?? "com.app") {
        self.service = service
    }

    func save(_ value: String, forKey key: String) {
        guard let data = value.data(using: .utf8) else { return }
        delete(forKey: key)

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
        ]
        SecItemAdd(query as CFDictionary, nil)
    }

    func load(forKey key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess, let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }

    func delete(forKey key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(query as CFDictionary)
    }
}

Usage

let keychain = KeychainWrapper()

// Save
keychain.save("my-api-token", forKey: "authToken")

// Load
if let token = keychain.load(forKey: "authToken") {
    print("Token: \(token)")
}

// Delete
keychain.delete(forKey: "authToken")

Codable Keychain Storage

For storing Codable objects:

func saveCodable<T: Encodable>(_ value: T, forKey key: String) {
    guard let data = try? JSONEncoder().encode(value) else { return }
    // Store data in keychain
}

func loadCodable<T: Decodable>(forKey key: String) -> T? {
    guard let data = loadData(forKey: key) else { return nil }
    return try? JSONDecoder().decode(T.self, from: data)
}

Quiz

1. What is Keychain Services used for?

Question 1 options

2. What function adds a new item to Keychain?

Question 2 options

3. What does kSecAttrAccessibleAfterFirstUnlock mean?

Question 3 options

4. How does Keychain data differ from UserDefaults?

Question 4 options

Flashcards

Question

What is Keychain Services?

Answer

Apple's encrypted storage for sensitive data like passwords, tokens, and keys. Data persists across app reinstalls.

Question

What is the difference between SecItemAdd and SecItemUpdate?

Answer

SecItemAdd creates a new keychain item. SecItemUpdate modifies an existing item.

Question

When should you use Keychain vs UserDefaults?

Answer

Keychain for sensitive data (tokens, passwords). UserDefaults for non-sensitive preferences.

Question

What does errSecDuplicateItem mean?

Answer

The keychain item already exists. You must update instead of add, or delete first.

Revision Notes

Key Takeaways

  • 1. Keychain provides encrypted storage for sensitive data that persists across app reinstalls
  • 2. SecItem API uses dictionaries for add, read, update, and delete operations
  • 3. Accessibility levels control when keychain data can be read
  • 4. Wrap Keychain operations in a reusable class for clean API
  • 5. Handle errSecDuplicateItem by updating instead of adding

Interview Tips

  • Explain when to use Keychain vs UserDefaults
  • Know the key SecItem functions and their parameters
  • Describe keychain accessibility levels and their security implications
  • Discuss keychain data migration when switching devices

Cheat Sheet

Keychain: Encrypted secure storage. SecItem API: Add, Copy, Update, Delete. kSecClass: GenericPassword, InternetPassword, Certificate, Key, Identity. kSecAttrAccessible: Controls when data is accessible. ErrSecDuplicateItem: Item exists, use update. Always handle SecItem status codes.