Persistent, Cross-Install Device Identifier on iOS: Using Keychain

The solution for the iOS Unique Device Identifier.

Miguel M. Almeida
5 min readMar 11, 2016

Getting a Unique Device Identifier in iOS has always been a controversial topic.

Until recently, device identification on iOS was a simple task. Everything up to iOS 4 offered the standard UDID (unique identifier per device) system, however for iOS 5 and onwards finding out who your users are has become more of a challenge.

Because Liquid relies on device identification for analytics, segmentation, and growth formulas, we thought it would be a good idea to share our experiences on how we have evolved and adapted to Apple’s changing ways.

Hopefully this will act as a guide to help you with device identification in your apps.

We begin our story with this controversy-causing quote from Apple:

“Do not use the uniqueIdentifier property. To create a unique identifier specific to your app, you can call the CFUUIDCreate function to create a UUID, and write it to the defaults database using the NSUserDefaults class.”

As of May 1st 2013 all apps that used uniqueIdentifier method were to be rejected from App Store.

Why? UDID was being mostly used to track user data by advertising services, crossing usage data between different apps: For instance, if you have a particular game installed, a shopping app, and a few newspaper apps, ad companies can extract a general idea of you from a marketing perspective.(source LifeHacker)

Apple left us two alternatives: IFA/IDFA (Identifier for Advertisers) and IFV/IDFV (Identifier for Vendor). However, both of them have problems:

  • IDFA is shared between all apps of the system, but can only be used by ad-enabled apps that are really showing the ads to the end user. Also, the user can opt-out and choose to reset it or disable the “across system” UID, causing a new UID to be generated for each install.
  • IDFV is shared between apps from the same publisher, but is lost when the last app of the publisher is uninstalled.

What this basically meant was that Apple had left us without a Device ID that is persistent if the app is uninstalled and installed again. We could save it at the NSUserDefaults, but it would also be lost when the app is uninstalled.

Solution using Keychain

Thankfully, there is a solution: to get a UID (IFA, IFV or generate your own) and store it in Keychain, like it was a password.

Keychain is mainly intended to securely store passwords, certificates and private keys. In iOS, Keychain items are not shared between apps (of different publishers) and, most importantly, are kept when the app is uninstalled. The best part is that the user doesn’t need to approve its access, give permissions or any kind of interaction. Everything is transparent. And no, the key will not be synchronized to iCloud by default as we’ll leave kSecAttrSynchronizable key enabled, so you don’t need to worry about having the same Device UID in two devices that install your app.

This turns Keychain into the perfect place to permanently store your Device UID.

Implementation

I’ve created a snippet with the implementation of this solution, and I’ll explain it below.

The first thing we’ll need to do is to retrieve the IFA, dynamically checking if AdSupport framework is available on the app (we choose IFA as default because it’s shared across apps):

+ (NSString *)appleIFA {
NSString *ifa = nil;
Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager");
if (ASIdentifierManagerClass) { // a dynamic way of checking if AdSupport.framework is available
SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager");
id sharedManager = ((id (*)(id, SEL))[ASIdentifierManagerClass methodForSelector:sharedManagerSelector])(ASIdentifierManagerClass, sharedManagerSelector);
SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier");
NSUUID *advertisingIdentifier = ((NSUUID* (*)(id, SEL))[sharedManager methodForSelector:advertisingIdentifierSelector])(sharedManager, advertisingIdentifierSelector);
ifa = [advertisingIdentifier UUIDString];
}
return ifa;
}

If IFA is not available, we’ll fallback to IFV:

+ (NSString *)appleIFV {
if(NSClassFromString(@"UIDevice") && [UIDevice instancesRespondToSelector:@selector(identifierForVendor)]) {
// only available in iOS >= 6.0
return [[UIDevice currentDevice].identifierForVendor UUIDString];
}
return nil;
}

The last fallback is to generate a new random UUID. Because NSUUID class was not available before iOS 6, in those cases, we’ll need to generate the UUID using CFUUIDRef.

#import <UIKit/UIDevice.h>+ (NSString *)randomUUID {
if(NSClassFromString(@"NSUUID")) { // only available in iOS >= 6.0
return [[NSUUID UUID] UUIDString];
}
CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef cfuuid = CFUUIDCreateString(kCFAllocatorDefault, uuidRef);
CFRelease(uuidRef);
NSString *uuid = [((__bridge NSString *) cfuuid) copy];
CFRelease(cfuuid);
return uuid;
}

Then, as soon as we have our UID, we’ll finally store it in Keychain, like we were talking about a password for a specific account or a specific service:

+ (void)setValue:(NSString *)value forKey:(NSString *)key inService:(NSString *)service {
NSMutableDictionary *keychainItem = [[NSMutableDictionary alloc] init];
keychainItem[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
keychainItem[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAlways;
keychainItem[(__bridge id)kSecAttrAccount] = key;
keychainItem[(__bridge id)kSecAttrService] = service;
keychainItem[(__bridge id)kSecValueData] = [value dataUsingEncoding:NSUTF8StringEncoding];
SecItemAdd((__bridge CFDictionaryRef)keychainItem, NULL);
}

and use it like, e.g. [self setValue:@”12345678–1234–1234–12345678” forKey:@”UID” inService:@”deviceInfo”];

Lastly, because Keychain data can be lost if you change the provisioning profile (not a common situation, but still), that’s a good idea to back it up to another place (e.g. NSUserDefaults) to retrieve it if you change the provisioning profile during an App version update:

[[NSUserDefaults standardUserDefaults] setObject:@”123456-1234-1234-12345678” forKey:@"deviceUID"];
[[NSUserDefaults standardUserDefaults] synchronize];

Connecting the wires

Well, it’s time to connect the wires. This gist contains all code, including some helper methods to store and retrieve the UID to and from NSUserDefaults and Keyhain.

From that class, the core methods that explain the workflow (a chain of UID fallbacks) is as simple as:

- (NSString *)uid {
if (!_uid) _uid = [[self class] valueForKeychainKey:_uidKey service:_uidKey];
if (!_uid) _uid = [[self class] valueForUserDefaultsKey:_uidKey];
if (!_uid) _uid = [[self class] appleIFA];
if (!_uid) _uid = [[self class] appleIFV];
if (!_uid) _uid = [[self class] randomUUID];
[self save];
return _uid;
}
- (void)save {
[DeviceUID setValue:_uid forUserDefaultsKey:_uidKey];
[DeviceUID setValue:_uid forKeychainKey:_uidKey inService:_uidKey];
}

Note that, for the sake of simplicity, some optimization code that avoids unnecessary saves was removed from the example above.

This is exactly the mechanism we’ve implemented at Liquid iOS SDK to track devices, and it is currently being used by all our customers. I’ve extracted it to a self contained DeviceUID class, and made it available here, so you can use it out-of-the-box, just by calling [DeviceUID uid] .

We’d love to hear and learn from you — let us know in the comments, we’re always looking for improvements on Liquid’s SDK.

Originally posted on Liquid Blog.

--

--