Keychain improvements in iOS 9
Keychain on iOS is unique. Interacting with it through the Security Framework has been much different from other frameworks on my mobile platform of choice. I’ve managed to get a hang of it and have written dozens of iterations of a sane Objective-C and Swift wrapper around it. Even though it’s still being updated, most of the new stuff is available through higher-level APIs, such as LocalAuthentication
framework.
In iOS 9 in particular a lot has changed and even if all the new features are not that hard to grasp, somehow a team dedicated to framework documentation seems to have vanished, since none of the new features are described in the official docs. Some bits and pieces are implemented in the keychain access example project and are touched on in the WWDC 2015 session 706 - “Security and your apps”.
Two of the new features caught my special interest.
How about I give you a finger
Touch ID has been a revelation in terms of ease, although its true security has been challenged many times. It’s not that your fingers are that hard to be detached from your hand, after all.
At least when it comes to it doing a good job protecting user’s secrets in A7’s secure enclave, there’s no contest as to whether the secrets are well hidden from prying eyes and forensic tools. Its ease of use though whets the appetite to being even simpler and transparent.
When you unlock your device using a finger and immediately are thrown into an app which protects its content using the same touch, you need to lift your finger and once again provide your precious fingerprint for analysis - that’s not really that bad. However, with some iOS 9 additions, we can relax app’s security a little bit by setting up the local authentication context to take into account that first unlocking touch for a few more seconds or minutes, even, to skip the second authentication step.
You just need to set LAContext’s touchIDAuthenticationAllowableReuseDuration
property to a chosen amount of seconds for the reuse period to last.
Passwords back in style
The other interesting new feature is securing the keychain items using an application-provided password, which is an additional factor to device unlock PIN or passcode.
The password can either come from the user or, as the session 706’s presenter suggested, either can be an additional token sent by the server or a secret kept on a device connected to the phone.
This security scheme requires at least a device passcode to exist, since it works with it in tandem. As a result of that it’s not possible to test it on the iOS Simulator, which makes developing the support for it a little harder, because when you add a keychain item protected that way, no actual password is even required to retrieve it.
Let’s create a function that adds an item protected with a password.
But first, minor setup. For example’s simplicity sake, I left out detailed error handling. Just visit OSStatus to dig into the errors returned, if needed.
enum Result {
case .Success(String?)
case .Failure(OSStatus)
}
OK, let’s get to work.
func addSecret(secret: String, usingPassword password : String) -> Result {
let secretData = secret.dataUsingEncoding(NSUTF8StringEncoding) // (1)
var error : Unmanaged<CFErrorRef>?
let acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.ApplicationPassword,
&error) // (2)
if error != nil {
return .Failure(errSecNotAvailable)
}
let context = LAContext()
context.setCredential(password.dataUsingEncoding(NSUTF8StringEncoding), type: .ApplicationPassword) // (3)
var attributes = [String : AnyObject]()
attributes[kSecClass as String] = kSecClassGenericPassword as String
attributes[kSecAttrAccount as String] = "exampleAccount"
attributes[kSecAttrService as String] = "secretService"
attributes[kSecValueData as String] = secretData
attributes[kSecAttrAccessControl as String] = acl // (4)
attributes[kSecUseAuthenticationContext as String] = context
let status = SecItemAdd(attributes, nil) // (5)
if status == errSecSuccess {
return .Success(nil) // (6)
} else {
return .Failure(status) // (6)
}
}
- The secret string is encoded into an NSData object in order to be serialized in the keychain.
.ApplicationPassword
type Access control object is created. In this example, we set a pretty robust accessibility level, i.e. the item is only accessible when the device is unlocked, the unlock passcode is set, and, of course, the application password provided matches the one the item was encrypted with.- A Local Authentication context is created with a password as a credential.
- Both the ACL and Authentication context are added as parameters to the keychain item.
- The item indicated by the Account and Service parameters is added to the keychain.
- If e.g. the item already exists, we will receive a status of
errSecDuplicateItem
. If nothing fails, we return a.Success
value.
Now, how do we get the secret out, then?
func retrieveSecretUsing(password : String) -> Result {
var error : Unmanaged<CFErrorRef>?
let acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.ApplicationPassword,
&error)
if error != nil {
return .Failure(errSecNotAvailable)
}
let context = LAContext()
context.setCredential(password.dataUsingEncoding(NSUTF8StringEncoding), type: .ApplicationPassword)
var attributes = [String : AnyObject]()
attributes[kSecClass as String] = kSecClassGenericPassword as String
attributes[kSecAttrAccount as String] = "exampleAccount"
attributes[kSecAttrService as String] = "secretService"
attributes[kSecReturnData as String] = kCFBooleanTrue // (1)
attributes[kSecMatchLimit as String] = kSecMatchLimitOne // (2)
attributes[kSecAttrAccessControl as String] = acl
attributes[kSecUseAuthenticationContext as String] = context
var resultEntry : AnyObject? = nil
let status = SecItemCopyMatching(attributes, &resultEntry)
if status == errSecSuccess,
let data = resultEntry as? NSData,
str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String { // (3)
return .Success(str)
}
return .Failure(status)
}
There are many analogies here to the addSecret
function, with the exception of some of the attributes passed to the SecItemCopyMatching function.
- We indicate that the call should return item’s data. Keychain can also return item’s properties if we wish.
- One matched item should be enough for everybody.
- We decode the retrieved NSData object into a String.
Now, only if we pass a correct password to the retrieveSecretUsing
function, we should receive the secret. Otherwise, an errSecAuthFailed
status, provided the item does already exist.
Remember - if run on the simulator, the function will always return the secret, regardless of the password provided.
Comments? Suggestions? Tweet me.