Weak Keychain Accessibility

ID

swift.weak_keychain_accessibility

Severity

high

Resource

Information leak

Language

Swift

Tags

CWE:311, CWE:312, CWE:359, MASWE:0006, MASWE:0007, NIST.SP.800-53, OWASP:2021:A2, OWASP:2021:A4, PCI-DSS:3.4, PCI-DSS:8.2.1

Description

Using weak Keychain accessibility settings allows sensitive data stored in the Keychain to be accessed even when the device is locked. This creates a significant security vulnerability because an attacker with physical access to a device could potentially extract Keychain items without needing to unlock the device.

The Keychain Services API provides several accessibility levels that control when stored items can be accessed. Some of these levels (particularly those marked as "always accessible") were designed for legacy compatibility and provide insufficient protection for modern security requirements.

Weak accessibility attributes include:

  • kSecAttrAccessibleAlways - Deprecated. Data is always accessible, even when the device is locked. This provides no data protection.

  • kSecAttrAccessibleAlwaysThisDeviceOnly - Data is always accessible even when locked, and not migratable to other devices. Still provides insufficient protection.

Rationale

The following example demonstrates vulnerable code that uses weak Keychain accessibility settings:

import Security
import Foundation

func saveAPIToken(token: String) {
    guard let data = token.data(using: .utf8) else {
        return
    }

    // VULNERABLE: Using kSecAttrAccessibleAlways
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "apiToken",
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAlways  // WEAK!
    ]

    SecItemDelete(query as CFDictionary)
    SecItemAdd(query as CFDictionary, nil)
}

func saveUserCredentials(password: String) {
    guard let data = password.data(using: .utf8) else {
        return
    }

    // VULNERABLE: Using kSecAttrAccessibleAlwaysThisDeviceOnly
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "userPassword",
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAlwaysThisDeviceOnly  // WEAK!
    ]

    SecItemAdd(query as CFDictionary, nil)
}

This code has several critical security problems:

  1. No lock-screen protection: The Keychain items can be accessed even when the device is locked, providing no protection against physical access attacks.

  2. Deprecated API usage: kSecAttrAccessibleAlways is deprecated by Apple because it provides insufficient security.

  3. Expanded attack surface: An attacker who gains temporary physical access to an unlocked device could install malware that later extracts the Keychain items while the device is locked.

  4. Insufficient data protection: The device’s data protection mechanisms are not fully leveraged, leaving sensitive data vulnerable.

  5. Forensic extraction: Law enforcement or forensic tools can extract these Keychain items more easily from locked devices.

Remediation

Use secure Keychain accessibility levels that require the device to be unlocked:

Option 1: kSecAttrAccessibleWhenUnlocked (Recommended for most cases)

This is the most secure option that balances security and usability. Data is only accessible when the device is unlocked:

import Security
import Foundation

class SecureKeychainStorage {
    func saveAPIToken(token: String) -> Bool {
        guard let data = token.data(using: .utf8) else {
            return false
        }

        // SECURE: Only accessible when device is unlocked
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "apiToken",
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]

        SecItemDelete(query as CFDictionary)
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }

    func getAPIToken() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "apiToken",
            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,
              let token = String(data: data, encoding: .utf8) else {
            return nil
        }

        return token
    }
}

Option 2: kSecAttrAccessibleAfterFirstUnlock (For background operations)

Use this when your app needs to access Keychain items in the background after the device has been unlocked at least once since boot:

func saveBackgroundSyncToken(token: String) -> Bool {
    guard let data = token.data(using: .utf8) else {
        return false
    }

    // SECURE: Accessible after first unlock (suitable for background tasks)
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "backgroundToken",
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
    ]

    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

Option 3: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly (Maximum security)

Use this for extremely sensitive data that should only be accessible when the device has a passcode set:

func saveBiometricKey(key: String) -> Bool {
    guard let data = key.data(using: .utf8) else {
        return false
    }

    // MAXIMUM SECURITY: Requires passcode, not migratable, only when unlocked
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "biometricKey",
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
    ]

    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

Comparison of Secure Accessibility Levels:

Accessibility Level When Accessible Migratable Use Case

kSecAttrAccessibleWhenUnlocked

Only when device is unlocked

Yes (to new devices)

General sensitive data

kSecAttrAccessibleWhenUnlockedThisDeviceOnly

Only when device is unlocked

No

Device-specific sensitive data

kSecAttrAccessibleAfterFirstUnlock

After first unlock since boot

Yes

Background operations

kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly

After first unlock since boot

No

Device-specific background data

kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly

When unlocked and passcode is set

No

Maximum security requirements

Best Practices:

  1. Choose appropriate accessibility: Use kSecAttrAccessibleWhenUnlocked for most sensitive data that doesn’t need background access.

  2. Consider "ThisDeviceOnly" variants: For highly sensitive data, use the "ThisDeviceOnly" variants to prevent migration to new devices or backups.

  3. Plan for background operations: If your app needs to access Keychain items during background operations, use kSecAttrAccessibleAfterFirstUnlock.

  4. Require passcode: For maximum security, use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly to ensure a device passcode is set.

  5. Add biometric protection: Consider using kSecAccessControlBiometryCurrentSet for additional biometric authentication requirements.

  6. Update legacy code: Replace any usage of deprecated kSecAttrAccessibleAlways immediately.

  7. Test lock-screen scenarios: Ensure your app handles cases where Keychain items are not accessible (e.g., when device is locked).

Configuration

This detector can be configured with the list of weak accessibility attributes to detect:

weakAccessibilityAttributes:
  - kSecAttrAccessibleAlways
  - kSecAttrAccessibleAlwaysThisDeviceOnly

You can add additional accessibility levels to this list if your security policy requires stricter controls.

References