< Back

# Wrapping up the iOS Keychain

What to expect

Using the keychain on iOS, when talking to fellow developers, always seems to be a tedious thing. If you’re not familiar with those APIs it might seem complex and opaque. But it really isn’t, let me show you in a very short example how you can leverage the iOS keychain. Tailored to your needs with just a few lines of code and without any third party dependencies.

let keychain = KeychainWrapper(service: "myService")

// Setting the Password for the Username
keychain["username"] = "12345"

// Retrieving the password for the Username
let password = keychain["username"]

In case you can’t wait, I’ve created a sample project here which contains the KeychainWrapper as well as a simple iOS App to try it out.

The KeychainWrapper I’m going to present to you will be as easy to use as the UserDefaults. It will be a pretty minimal implementation of the existing iOS Keychain features and you might want to add more functionality to it depending on your needs, but I’m pretty sure in most of the simple use-cases this implementation will be absolutely sufficient.

Query all the things

First of all, let’s look at the keychainQuery which we’ll be using for either creating new, updating existing or deleting keychain items within our App. It’ll be vital for working with any keychain operation.

[
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: service,
    kSecAttrAccount as String: key,
    kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
    kSecAttrSynchronizable as String: kCFBooleanTrue!
]

We’re only going to deal with passwords to be stored as credentials here, so we’re going to set the class item to kSecClassGenericPassword which basically tells the keychain that this is item is a generic password, this has implications on the attributes we’re going to use.

The first attribute we’re going to define is called kSecAttrService which describe the service (or scope) we’re going to use this item in, you could e.g. set this to the consumer of the credentials, for example you’re App’s “Loggedin-User-Only”-Area etc.

The attribute kSecAttrAccount we’ll be using for the user’s username in this case.

Another important attribute is kSecAttrAccessible which restricts certain usage of the keychaim item, e.g. if the device is locked and has never been unlocked, for example after a reboot. There is a variety of possible values for this attribute, we’re going to use kSecAttrAccessibleAfterFirstUnlock which means that the devices needs to be unlocked at least once for the item to be used.

And at last we’re going to set kSecAttrSynchronizable to true to allow the item be synchronized to other devices using iCloud.

Creating items

To insert items into the keychain, we’re first going to check if an item with the same username is already existing, if it isn’t we’re going to insert one using the query properties we’ve defined previously.

guard SecItemCopyMatching(query as CFDictionary, nil) == noErr else {
    query[kSecValueData as String] = data
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

Now let’s see what’s going here, SecItemCopyMatching is querying for an existing item in the keychain and, if successful, returning it. In our case if this fails, which means the item is not yet existing, we’re going to add a new item using SecItemAdd which takes our predefined query plus the added kSecValueData which is the actual password we’re going to store for this username.

As we’re dealing with CoreFoundation here we have to cast our Swift dictionaries to CFDictionary.

We’re also not providing the second, optional, parameter to SecItemAdd which would return a pointer to the newly created object.

Updating items

if SecItemCopyMatching(query, nil) == noErr {
    return SecItemUpdate(
        query as CFDictionary,
        NSDictionary(dictionary: [kSecValueData: data])
    ) == errSecSuccess
}

To update existing items (after checking for existence) just pass in the query and an updated dictionary containing the new password (kSecValueData).

Deleting items

if SecItemCopyMatching(query, nil) == noErr {
    return SecItemDelete(query as CFDictionary) == noErr
}

Very straight forward as well is deleting existing items, just pass in the query you’d use to search for an item, but using the SecItemDelete method. And voila, it’s gone.

One more thing…

To make this little wrapper slightly more friendly we’re going to add some API sugar on top, using a custom subscript.

subscript(key: KeychainKey) -> String? {
    get {
        return get(stringForKey: key)
    } set {
        guard let value = newValue else {
            del(valueForKey: key)
            return
        }
        set(value, forKey: key)
    }
}

We just want to be able to set a value on our KeychainWrapper instance and have it persisted inside the keychain e.g.:

keychain["username"] = password

That’s about it for a fully functional, yet slim implementation of an iOS keychain. The system keychain is more powerful than a single blogpost could do justice for, but I hope you’ve got an idea of how to write your own, tailored to your needs, implementation now.

I’ve created a sample project here which contains the KeychainWrapper as well as a simple iOS App to try it out.

I would love to hear your opinion on this so please feel free to drop me a line on Mastodon or via Email.

Putting all the bits and pieces together

And here’s the full implementation of the KeychainWrapper:

import Foundation

typealias KeychainService = String
typealias KeychainKey = String

class KeychainWrapper {
    let service: String
    
    init(service: String) {
        self.service = service
    }
    
    private func keychainQuery(withService service: KeychainService, forKey key: KeychainKey) -> [String: Any] {
        return [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
            kSecAttrSynchronizable as String: kCFBooleanTrue!
        ]
    }
    
    @discardableResult
    func set(_ string: String, forKey key: KeychainKey) -> Bool {
        var query = keychainQuery(withService: service, forKey: key)
        
        guard let data = string.data(using: .utf8) else {
            return false
        }
        
        guard SecItemCopyMatching(query as CFDictionary, nil) == noErr else {
            query[kSecValueData as String] = data
            let status = SecItemAdd(query as CFDictionary, nil)
            return status == errSecSuccess
        }
        
        return SecItemUpdate(
            query as CFDictionary,
            NSDictionary(dictionary: [kSecValueData: data])
        ) == errSecSuccess
    }
    
    func get(stringForKey key: KeychainKey) -> String? {
        var query = keychainQuery(withService: service, forKey: key)
        query[kSecReturnData as String] = kCFBooleanTrue
        query[kSecReturnAttributes as String] = kCFBooleanTrue
        
        var result: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &result) == noErr else {
            return nil
        }
        
        guard
            let dictionary = result as? [String: Any],
            let data = dictionary[kSecValueData as String] as? Data
        else {
            return nil
        }
        
        return String(data: data, encoding: .utf8)
    }
    
    @discardableResult
    func del(valueForKey key: KeychainKey) -> Bool {
        let query = keychainQuery(withService: service, forKey: key)
        return SecItemDelete(query as CFDictionary) == noErr
    }
    
    subscript(key: KeychainKey) -> String? {
        get {
            return get(stringForKey: key)
        } set {
            guard let value = newValue else {
                del(valueForKey: key)
                return
            }
            set(value, forKey: key)
        }
    }
}

I would love to hear your opinion on this so please feel free to drop me a line on Mastodon or via Email.

Cheers, Marcus

Impressum • Mastodon