Valet: Keychain done right.

Needless to say, as a bank, we need to have "Bank Grade Security".

No, not THAT Bank Grade Security, but we do need to secure things which should be secret[1].

The main thing we take care to secure is our device tokens, which are a set of secrets that, when combined with something like a PIN, will let you into the application, and save the user having to enter their username and password every time.

On iOS, the obvious choice here is to use the Keychain.

Which of course we were using, via a very, very old wrapper class called SFHFKeychainUtils, which has a header date in 2008. I'm not even sure if it's aware of ARC.

So, this time of the year is a GREAT time for upgrades of low level things like this. There are a lot of people off work, so if we break something, we don't stop too many people working, and we are about to wrap up a release, so if we make the change shortly (in this case, next week) we'll have a full release cycle + regression to shake out any bugs. Perfect.

In the place of SFHFKeychainUtils, we have chosen to use Squares Valet library.

Importing it, as we use Cocoapods for other stuff, is as easy as a one-liner in the Podfile:

pod 'Valet'

and a quick pod update. Once it's in there, you can use it like a fairly normal key-value store, ala NSUserDefaults. Once we have setup a Valet...

self.valet = [[VALValet alloc] initWithSharedAccessGroupIdentifier:@"com.foo.SharedKeychain" accessibility:VALAccessibilityAfterFirstUnlock];

... we can read and write ...

NSString *token = [self.valet stringForKey:@"token"];
...
[self.valet setString:@"token_value" forKey:@"token"];

Valet has a few different "Valets" it comes with:

  • VALValet is the normal one, just reads and writes the Keychain, and this can be local only, shared amongst your other apps, and have various accessibility levels like "anytime" and "anytime after the phone has been unlocked once". Most of these are standard keychain rules.
  • VALSynchronizableValet which sets the Valet up to sync via iCloud
  • VALSecureEnclaveValet which stores the items in the Secure Enclave, which means you have to Touch Id authenticate, or enter your device PIN, to get the value out.

UPDATE: In Valet 2.2.0, they included the "finger print only" option, so most of this isn't needed. See the extra bit at the bottom for more info

That covers around 99% of uses. Sadly, we need the other 1%! Because 'reasons', we need to prevent the "device PIN" aspect of the VALSecureEnclaveValet process. On the plus side, with how Valet is written, this is as easy as making a class which descends from VALSecureEnclaveValet and overriding one method:

- (nonnull NSMutableDictionary *)mutableBaseQueryWithIdentifier:(nonnull NSString *)identifier initializer:(SEL)initializer accessibility:(VALAccessibility)accessibility;
{

    //our override is:
    //get the old base query. Then remove the Access Control, and add our own one, which is the one which will NOT fail back to
    // pin if they get it wrong etc.
    //iOS9 only, BTW, tho we are not checking, as we check elsewhere.

    NSMutableDictionary *res = [super mutableBaseQueryWithIdentifier:identifier initializer:initializer accessibility:accessibility];

    [res removeObjectForKey:(__bridge id)kSecAttrAccessControl];

    res[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, NULL);

    return res;
}

Thats it. The combination of kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly and kSecAccessControlTouchIDCurrentSet forces ONLY fingerprints, and also is disabled if the set of fingerprints change (there is another one which doesn't change when the print list changes - we didn't want that).

Nice and easy.

UPDATE

In Valet 2.2.0, they have added support for fingerprint only and "did the print change", so you can alloc a new Valet using

valet = [[VALSecureEnclaveValet alloc] initWithIdentifier:@"Your_Id_Here" accessControl:VALAccessControlTouchIDCurrentFingerprintSet];            

And this will stop working when the print set changed. There is also VALAccessControlTouchIDAnyFingerprint which is still Touch ID only, but doesn't invalidate when you add or remove one.

The other change we needed was how to detect if the fingerprints have changed. To do that, we just use containsObjectForKey: to check for the item first. If it's not there, then either they haven't set it up yet (and we have another flag for that), or the fingerprint list has changed.

if (![valet containsObjectForKey:KEY_NAME]) {
    //we can tell if it's there without auth.
    // so if it's not, the print set changed
    failureHandler(TouchIdPrintSetInvalid);
}

NSString *token = [valet stringForKey:KEY_NAME userPrompt:PROMPT_TEXT];

if (token) {
    successHandler(token);
} else {
    failureHandler(UserPressedCancel);
}

We also had to migrate our old settings from the existing keychain over to the new one, as the names were a bit different. That was as easy as read - check - write, tho Valet has some functions which will migrate for you.

Looking at the code, it's VERY clean, and has a lot of "make sure it's on the right thread and is locked during use" stuff. The code, in general, the the single most understandable piece of Keychain code I've ever seen. Well done those people.

So, please, use the keychain for anything remotely important. Really, you could use it for any and every setting you have which doesn't need to be set in the iOS Settings app - it's performant, and with Valet, easy to use. No Excuses.


  1. The ANZ in that list is ANZ Australia, not ANZ New Zealand. Our SSL grade isn't a lot better, to be honest, tho we are actively working on changing that - sadly, due to "enterprise" it's not as easy as just installing a new certificate and configuring the front end SSL terminator. Tho I think it should be. ↩︎