After a solid iOS developer?

I am an iOS developer / contractor based in Sydney, Australia.
I specialise in building well-structured app platforms for companies to build their long-term app strategies upon.
I have a broad array of experience from my work at Google, News Corp, Fox Sports, NineMSN, FetchTV, Woolworths, and Westpac.
Please see my portfolio, then get in touch if I can be of service!

See my Portfolio »

Swift Keychain wrapper

If, like most Apps, you need to store something in the Keychain, hopefully I can help you out here. This post is written because most Keychain-related cocoapods don't handle the three modern requirements:

  • Cloud sync via iCloud Keychain
  • Enabling background access for background-fetch apps
  • Swift (version 2 as of this writing)

Plus, I'm a fan of writing the minimum code that can do the job, and customising per requirements if necessary, rather than bringing in a large cocoapod with more features than you'll ever need.

Having said that, BMCredentials is awesome on the first two points if you don't mind a bit of Objective-C in your new project. Written by a friend though so i'm a tad biased ;)

Now, before we get stuck in, you should consider watching Security and Your Apps from WWDC 2015 and reading the slides, which provides the inspiration for some of this article.

All the source code including unit tests is available here: github.com/chrishulbert/Swift2Keychain.

Bridging SecItemX to Swift

The first hurdle is calling the four SecItem functions from Swift:

  • SecItemAdd
  • SecItemDelete
  • SecItemUpdate
  • SecItemCopyMatching

These functions are a C library, returning data via unmanaged double-pointers and non-enum error codes. So I'll show you how to make a slim wrapper that converts them to return values straightforwardly, and throws errors as a nicely-typed enum. Firstly, here is the error enumeration that maps from the errSecX values:

import Security

enum KeychainError: ErrorType {
    case Unimplemented
    case Param
    case Allocate
    case NotAvailable
    case AuthFailed
    case DuplicateItem
    case ItemNotFound
    case InteractionNotAllowed
    case Decode
    case Unknown

    /// Returns the appropriate error for the status, or nil if it
    /// was successful, or Unknown for a code that doesn't match.
    static func errorFromOSStatus(rawStatus: OSStatus) ->
            KeychainError? {
        if rawStatus == errSecSuccess {
            return nil
        } else {
            // If the mapping doesn't find a match, return unknown.
            return mapping[rawStatus] ?? .Unknown
        }
    }

    static let mapping: [Int32: KeychainError] = [
        errSecUnimplemented: .Unimplemented,
        errSecParam: .Param,
        errSecAllocate: .Allocate,
        errSecNotAvailable: .NotAvailable,
        errSecAuthFailed: .AuthFailed,
        errSecDuplicateItem: .DuplicateItem,
        errSecItemNotFound: .ItemNotFound,
        errSecInteractionNotAllowed: .InteractionNotAllowed,
        errSecDecode: .Decode
    ]
}

I really love how Swift allows us to place the errorFromOSStatus helper inside the enum, as well as the static mapping dictionary. Now it's worth mentioning that I tried to make the enum with the same base type as the OSStatus, and set each case's raw value to the corresponding errSecX value. However, this only worked if I used the actual integer values themselves, I couldn't use the errSecX constants, which I thought was a code smell, and instead settled on using the above mapping instead. I'll understand if you disagree.

Bridging the SecItemX methods

Next are my helpers that simply pass through to the SecItem methods, returning the result straightforwardly and throwing if there is an error. This is also a good example of how to call C functions that return data via a double pointer from Swift:

struct SecItemWrapper {
    static func matching(query: [String: AnyObject]) throws -> AnyObject? {
        var rawResult: Unmanaged<AnyObject>?
        let rawStatus = SecItemCopyMatching(query, &rawResult)
        // Immediately take the retained value, so it won't leak
        // in case it needs to throw.
        let result: AnyObject? = rawResult?.takeRetainedValue()

        if let error = KeychainError.errorFromOSStatus(rawStatus) {
            throw error
        }
        return result
    }

    static func add(attributes: [String: AnyObject]) throws -> AnyObject? {
        var rawResult: Unmanaged<AnyObject>?
        let rawStatus = SecItemAdd(attributes, &rawResult)
        let result: AnyObject? = rawResult?.takeRetainedValue()

        if let error = KeychainError.errorFromOSStatus(rawStatus) {
            throw error
        }
        return result
    }

    static func update(query: [String: AnyObject],
            attributesToUpdate: [String: AnyObject]) throws {
        let rawStatus = SecItemUpdate(query, attributesToUpdate)
        if let error = KeychainError.errorFromOSStatus(rawStatus) {
            throw error
        }
    }

    static func delete(query: [String: AnyObject]) throws {
        let rawStatus = SecItemDelete(query)
        if let error = KeychainError.errorFromOSStatus(rawStatus) {
            throw error
        }
    }
}

In short, you created a typed, unmanaged optional: var x: Unmanaged<T>?. You pass this in via the & operator. And due to the 'create rule' which applies, the returned value has a +1 retain count, which we balance by calling takeRetainedValue.

I expect that in a future version of Swift, Apple will tidy up the way that C methods such as these return unsafe unmanaged pointers to something simpler, and the above code will become shorter.

Convenience methods

Now that you've got the bridge in place, it's a matter of adding some convenience methods for typical keychain operations:

struct Keychain {

    static func deleteAccount(account: String) {
        do {
            try SecItemWrapper.delete([
                kSecClass as String: kSecClassGenericPassword,
                kSecAttrService as String: Constants.service,
                kSecAttrAccount as String: account,
                kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
            ])
        } catch KeychainError.ItemNotFound {
            // Ignore this error.
        } catch let error {
            NSLog("deleteAccount error: \(error)")
        }
    }

    static func dataForAccount(account: String) -> NSData? {
        do {
            let query = [
                kSecClass as String: kSecClassGenericPassword,
                kSecAttrService as String: Constants.service,
                kSecAttrAccount as String: account,
                kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
                kSecReturnData as String: kCFBooleanTrue as CFTypeRef,
            ]
            let result = try SecItemWrapper.matching(query)
            return result as? NSData
        } catch KeychainError.ItemNotFound {
            // Ignore this error, simply return nil.
            return nil
        } catch let error {
            NSLog("dataForAccount error: \(error)")
            return nil
        }
    }

    static func stringForAccount(account: String) -> String? {
        if let data = dataForAccount(account) {
            return NSString(data: data,
                encoding: NSUTF8StringEncoding) as? String
        } else {
            return nil
        }
    }

    static func setData(data: NSData,
            forAccount account: String,
            synchronizable: Bool,
            background: Bool) {
        do {
            // Remove the item if it already exists.
            // This saves having to deal with SecItemUpdate.
            // Reasonable people may disagree with this approach.
            deleteAccount(account)

            // Add it.
            try SecItemWrapper.add([
                kSecClass as String: kSecClassGenericPassword,
                kSecAttrService as String: Constants.service,
                kSecAttrAccount as String: account,
                kSecAttrSynchronizable as String: synchronizable ?
                    kCFBooleanTrue : kCFBooleanFalse,
                kSecValueData as String: data,
                kSecAttrAccessible as String: background ?
                    kSecAttrAccessibleAfterFirstUnlock :
                    kSecAttrAccessibleWhenUnlocked,
            ])
        } catch let error {
            NSLog("setData error: \(error)")
        }
    }

    static func setString(string: String,
            forAccount account: String,
            synchronizable: Bool,
            background: Bool) {
        let data = string.dataUsingEncoding(NSUTF8StringEncoding)!
        setData(data,
            forAccount: account,
            synchronizable: synchronizable,
            background: background)
    }

    struct Constants {
        // FIXME: Change this to the name of your app or company!
        static let service = "MyService"
    }
}

Some things to note:

  • Make sure you change the service name in the Constants struct above!
  • kSecAttrSynchronizable: kSecAttrSynchronizableAny is essential in all queries because SecItem.h says: "If the key is not supplied... then no synchronizable items will be added or returned".
  • Everything is stored in the keychain as NSData natively, and you must convert to and from strings using UTF-8 encoding.
  • SecItemAdd will fail if the item already exists, so I simply delete before adding always. Other libraries take the more complicated route of checking if present, if so calling SecItemUpdate, otherwise SecItemAdd. You may prefer that.
  • Pass true for synchronizable if you want this entry to be synced to the user's other devices via iCloud.
  • Pass true for background if you'd like the entry to be used while your app is in the background, eg background fetch. Keep in mind you still won't be able to get the value if the user does a fresh reboot and hasn't launched your app yet, so you have to handle that gracefully for eg users who turn their devices off at night.
  • If you want to read more about using the keychain, read SecItem.h.
  • Source code and unit tests are available here: github.com/chrishulbert/Swift2Keychain.

Hope someone finds this helpful :)


You can read more of my blog here in my blog archive.