Weak Keychain ACL (Device Passcode)

ID

swift.weak_keychain_acl_device_passcode

Severity

high

Resource

Authentication

Language

Swift

Tags

CWE:305, CWE:320, MASVS:auth-8, MASWE:0014, NIST.SP.800-53, OWASP:2021:A07, PCI-DSS:8.2.1

Description

When storing sensitive cryptographic keys in the iOS Keychain, developers can specify access control lists (ACLs) that determine how users authenticate to access those keys. Using .devicePasscode as the access control flag provides weaker security compared to biometric-based authentication methods like Face ID or Touch ID.

The SecAccessControlCreateWithFlags function allows developers to specify authentication requirements for Keychain items. While device passcodes provide some protection, they are significantly more vulnerable to attacks than biometric authentication:

  • Shoulder surfing: Passcodes can be observed when entered in public

  • Social engineering: Users can be coerced or tricked into revealing passcodes

  • Brute force: Short passcodes are vulnerable to brute force attacks

  • Physical security: Passcodes lack the hardware-backed security of biometric authentication

Weak ACL flags include:

  • .devicePasscode - Allows access with device passcode only, bypassing stronger biometric authentication

Rationale

The following example demonstrates vulnerable code that uses device passcode for Keychain ACL:

import Security
import Foundation

func createWeakKeychainKey() throws {
    // VULNERABLE: Using .devicePasscode for access control
    guard let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .devicePasscode,  // WEAK! Vulnerable to shoulder surfing
        nil
    ) else {
        throw KeychainError.accessControlCreationFailed
    }

    let privateKeyAttrs: [String: Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: "com.example.key".data(using: .utf8)!,
        kSecAttrAccessControl as String: accessControl
    ]

    let attributes: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits as String: 256,
        kSecPrivateKeyAttrs as String: privateKeyAttrs
    ]

    var error: Unmanaged<CFError>?
    guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
        throw KeychainError.keyGenerationFailed
    }
}

func storeEncryptionKeyWithPasscode(keyData: Data) throws {
    // VULNERABLE: Combining .devicePasscode with other flags
    guard let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
        [.devicePasscode, .privateKeyUsage],  // WEAK!
        nil
    ) else {
        throw KeychainError.accessControlCreationFailed
    }

    let query: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: "com.example.encryption".data(using: .utf8)!,
        kSecValueData as String: keyData,
        kSecAttrAccessControl as String: accessControl
    ]

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.storageFailed
    }
}

This code has several critical security problems:

  1. Vulnerable to observation attacks: Device passcodes can be observed through shoulder surfing, especially in public spaces.

  2. Weaker than biometric authentication: Face ID and Touch ID use hardware-backed security (Secure Enclave) that is significantly harder to bypass.

  3. Social engineering vulnerability: Users can be coerced or tricked into revealing their passcode, but cannot easily share biometric data.

  4. No hardware-backed security: Unlike biometric authentication, passcode verification doesn’t leverage the Secure Enclave’s cryptographic operations.

  5. Reduced user awareness: Passcode entry is a common operation, making users less likely to notice when sensitive keys are being accessed.

Remediation

Use biometric-based access control flags for stronger security:

Option 1: .biometryCurrentSet (Recommended)

This is the most secure option that requires current biometric enrollment (Face ID or Touch ID):

import Security
import Foundation

enum KeychainError: Error {
    case accessControlCreationFailed
    case keyGenerationFailed
    case storageFailed
}

class SecureKeychainStorage {
    func createSecureKeychainKey() throws {
        // SECURE: Using .biometryCurrentSet for access control
        guard let accessControl = SecAccessControlCreateWithFlags(
            kCFAllocatorDefault,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            .biometryCurrentSet,  // Requires current biometric enrollment
            nil
        ) else {
            throw KeychainError.accessControlCreationFailed
        }

        let privateKeyAttrs: [String: Any] = [
            kSecAttrIsPermanent as String: true,
            kSecAttrApplicationTag as String: "com.example.key".data(using: .utf8)!,
            kSecAttrAccessControl as String: accessControl
        ]

        let attributes: [String: Any] = [
            kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeySizeInBits as String: 256,
            kSecPrivateKeyAttrs as String: privateKeyAttrs
        ]

        var error: Unmanaged<CFError>?
        guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
            throw KeychainError.keyGenerationFailed
        }

        print("✅ Secure key created with biometric protection")
    }

    func storeEncryptionKeyWithBiometrics(keyData: Data) throws {
        // SECURE: Using .biometryCurrentSet
        guard let accessControl = SecAccessControlCreateWithFlags(
            kCFAllocatorDefault,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            [.biometryCurrentSet, .privateKeyUsage],
            nil
        ) else {
            throw KeychainError.accessControlCreationFailed
        }

        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: "com.example.encryption".data(using: .utf8)!,
            kSecValueData as String: keyData,
            kSecAttrAccessControl as String: accessControl
        ]

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw KeychainError.storageFailed
        }

        print("✅ Encryption key stored with biometric protection")
    }
}

Option 2: .biometryAny (For compatibility)

Use this when you want to allow any enrolled biometric, including those enrolled after key creation:

func createKeychainKeyWithBiometryAny() throws {
    // SECURE: Using .biometryAny for backward compatibility
    guard let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .biometryAny,  // Allows any enrolled biometric
        nil
    ) else {
        throw KeychainError.accessControlCreationFailed
    }

    let privateKeyAttrs: [String: Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: "com.example.flexible".data(using: .utf8)!,
        kSecAttrAccessControl as String: accessControl
    ]

    let attributes: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits as String: 256,
        kSecPrivateKeyAttrs as String: privateKeyAttrs
    ]

    var error: Unmanaged<CFError>?
    _ = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
}

Option 3: .userPresence (Balanced approach)

Use this to require biometric or passcode authentication as a fallback:

func createKeychainKeyWithUserPresence() throws {
    // ACCEPTABLE: Using .userPresence (biometric or passcode fallback)
    guard let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .userPresence,  // Biometric preferred, passcode as fallback
        nil
    ) else {
        throw KeychainError.accessControlCreationFailed
    }

    let privateKeyAttrs: [String: Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: "com.example.balanced".data(using: .utf8)!,
        kSecAttrAccessControl as String: accessControl
    ]

    let attributes: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits as String: 256,
        kSecPrivateKeyAttrs as String: privateKeyAttrs
    ]

    var error: Unmanaged<CFError>?
    _ = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
}

Comparison of Access Control Flags:

Access Control Flag Authentication Required Security Level

.devicePasscode

Device passcode only

⚠️ Weak (vulnerable to observation)

.userPresence

Biometric or passcode fallback

🟡 Moderate (biometric preferred)

.biometryAny

Any enrolled biometric

✅ Strong (hardware-backed)

.biometryCurrentSet

Current biometric enrollment only

✅ Strongest (invalidates on biometric change)

Best Practices:

  1. Prefer biometric authentication: Use .biometryCurrentSet for maximum security with hardware-backed protection.

  2. Consider user experience: .biometryAny provides better UX by allowing new biometric enrollments without invalidating keys.

  3. Use .userPresence carefully: While .userPresence provides passcode fallback for accessibility, it’s stronger than .devicePasscode alone since biometric is preferred.

  4. Never use .devicePasscode for sensitive keys: Cryptographic keys, authentication tokens, and sensitive credentials should always use biometric protection.

  5. Handle biometric enrollment: Check if biometrics are enrolled before using biometric-only flags, and provide clear error messages.

  6. Combine with proper accessibility: Always use appropriate kSecAttrAccessible values (like kSecAttrAccessibleWhenUnlockedThisDeviceOnly) alongside ACL flags.

  7. Test fallback scenarios: Ensure your app handles cases where biometric authentication fails or is unavailable.

Configuration

This detector can be configured with the list of weak ACL flags to detect:

weakAclFlags:
  - devicePasscode

You can add additional ACL flags to this list if your security policy requires stricter controls.