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:
-
No lock-screen protection: The Keychain items can be accessed even when the device is locked, providing no protection against physical access attacks.
-
Deprecated API usage:
kSecAttrAccessibleAlwaysis deprecated by Apple because it provides insufficient security. -
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.
-
Insufficient data protection: The device’s data protection mechanisms are not fully leveraged, leaving sensitive data vulnerable.
-
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 |
|---|---|---|---|
|
Only when device is unlocked |
Yes (to new devices) |
General sensitive data |
|
Only when device is unlocked |
No |
Device-specific sensitive data |
|
After first unlock since boot |
Yes |
Background operations |
|
After first unlock since boot |
No |
Device-specific background data |
|
When unlocked and passcode is set |
No |
Maximum security requirements |
Best Practices:
-
Choose appropriate accessibility: Use
kSecAttrAccessibleWhenUnlockedfor most sensitive data that doesn’t need background access. -
Consider "ThisDeviceOnly" variants: For highly sensitive data, use the "ThisDeviceOnly" variants to prevent migration to new devices or backups.
-
Plan for background operations: If your app needs to access Keychain items during background operations, use
kSecAttrAccessibleAfterFirstUnlock. -
Require passcode: For maximum security, use
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyto ensure a device passcode is set. -
Add biometric protection: Consider using
kSecAccessControlBiometryCurrentSetfor additional biometric authentication requirements. -
Update legacy code: Replace any usage of deprecated
kSecAttrAccessibleAlwaysimmediately. -
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
-
CWE-311: Missing Encryption of Sensitive Data.
-
CWE-312: Cleartext Storage of Sensitive Information.
-
CWE-359: Exposure of Private Personal Information to an Unauthorized Actor.
-
OWASP Top 10 2021 A02 - Cryptographic Failures.
-
OWASP Top 10 2021 A04 - Insecure Design.
-
MASWE-0006: Sensitive Data Stored Unencrypted in Private Storage Locations.
-
MASWE-0007: Sensitive Data Stored Unencrypted in Shared Storage Requiring No User Interaction.