iOS 8 (OS X Yosemite) introduced a new API/constant used to detect if a user's device has a passcode.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
can be used to detect if a passcode is set on the device.
The flow is:
- Attempt to save a new item on the keychain with that attribute set
- If it succeeds that indicates that a passcode is currently enabled
- If the password doesn't get saved, that indicates there is no passcode
- Cleanup the item, because if it is already on the keychain it will make an "add" fail, looking like the passcode is not set
I have tested this on my iPhone 5S, first it returned true
, then I disabled the passcode in settings, and it returned false
. Finally, I re-enabled the passcode and it returns true
. Prior OS versions will return false
. The code works in simulator, returning true
on a machine with OS X password set (I haven't tested alternate OS X scenarios).
Also see sample project here: https://github.com/project-imas/passcode-check/pull/5
Finally, to my knowledge iOS 8 doesn't have a setting to disable data protection, so I assume this is all you need to guarantee encryption.
BOOL isAPIAvailable = (&kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly != NULL);
// Not available prior to iOS 8 - safe to return false rather than crashing
if(isAPIAvailable) {
// From http://pastebin.com/T9YwEjnL
NSData* secret = [@"Device has passcode set?" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *attributes = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"LocalDeviceServices",
(__bridge id)kSecAttrAccount: @"NoAccount",
(__bridge id)kSecValueData: secret,
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
};
// Original code claimed to check if the item was already on the keychain
// but in reality you can't add duplicates so this will fail with errSecDuplicateItem
// if the item is already on the keychain (which could throw off our check if
// kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly was not set)
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
if (status == errSecSuccess) { // item added okay, passcode has been set
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"LocalDeviceServices",
(__bridge id)kSecAttrAccount: @"NoAccount"
};
status = SecItemDelete((__bridge CFDictionaryRef)query);
return true;
}
// errSecDecode seems to be the error thrown on a device with no passcode set
if (status == errSecDecode) {
return false;
}
}
return false;
P.S. As Apple points out in the WWDC video introducing this (711 Keychain and authentication with Touch ID), they chose not to make the passcode-status directly available via API on purpose, in order to prevent apps from getting in situations they shouldn't be (i.e "Does this device have a passcode? Okay, great, I'll store this private info in plain text". It would be much better to create an encryption key, store it under kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
and encrypt that file, which will be unrecoverable if a user decides to disable their passcode).