Swift Keychain

The iOS keychain is a great place for storing sensitive information that you don’t feel safe storing in UserDefaults or in a file on disk. Things like login details, passwords, auth tokens are perfectly suited for storing in the Keychain.

In this article, I’m going to show how to use the Keychain directly from Swift without dropping down to Objective-C or using a cocoapod/carthage library. Keep in mind that Apple recommends having less than 10 framework libraries with your app, or you’ll suffer performance issues. With my example, simply create a KeychainWrapper.swift file in your project, paste in my sample code, modify as you see fit, and you’re off to the races.

To be honest, iOS devices have a high level of security nowadays (as of writing) and I’m hard-pressed to give great reasons why information in the Keychain is any more secure than a file saved in your app’s Library folder. Nevertheless, it seems to be best practice to use the Keychain. Perhaps people who know why the keychain is better can let me know and I’ll update this article. Update: I’m told that there are exploits that can dump the filesystem, but not the keychain, but I’ve also heard that the filesystem has been fully encrypted for years now too, so I’m really on the fence about this one.

Things to know

  • The keychain stores data, not strings, so you’re responsible for calling myString.data(using: .utf8) and String(data: keychainData, encoding: .utf8) when calling it with string data.
  • When an item is added, you have the chance to specify when you want to be able to read the item. If your app ever runs in the background, eg to handle push notifications, you’ll need to set kSecAttrAccessibleAfterFirstUnlock.
  • In a WWDC video, Apple confirmed once that your app will never be run in the background before the first unlock, so the above option is recommended.
  • If you want your item to not sync via iCloud to the user’s other devices, you can specify kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly instead.
  • I only use the ‘kSecClassGenericPassword’ class of keychain item for simplicity. This ‘class’ has, as its primary key, a composite of the two ‘service’ and ‘account’ fields. Thus, in my code below, wherever you see ‘account’ think ‘key’, because I’m using a constant value for service.
  • Other classes have primary keys made up of (multiple) different fields.
  • iOS keeps your keychain items even when you delete and reinstall your app, in my testing.

The code

import Foundation
import Security

// You might want to update this to be something descriptive for your app.
private let service: String = "MyService"

enum Keychain {

    /// Does a certain item exist?
    static func exists(account: String) throws -> Bool {
        let status = SecItemCopyMatching([
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            kSecReturnData: false,
            ] as NSDictionary, nil)
        if status == errSecSuccess {
            return true
        } else if status == errSecItemNotFound {
            return false
        } else {
            throw Errors.keychainError
        }
    }
    
	/// Adds an item to the keychain.
    private static func add(value: Data, account: String) throws {
        let status = SecItemAdd([
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
			// Allow background access:
            kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
            kSecValueData: value,
            ] as NSDictionary, nil)
        guard status == errSecSuccess else { throw Errors.keychainError }
    }
    
	/// Updates a keychain item.
    private static func update(value: Data, account: String) throws {
        let status = SecItemUpdate([
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            ] as NSDictionary, [
            kSecValueData: value,
            ] as NSDictionary)
        guard status == errSecSuccess else { throw Errors.keychainError }
    }
    
    /// Stores a keychain item.
    static func set(value: Data, account: String) throws {
        if try exists(account: account) {
            try update(value: value, account: account)
        } else {
            try add(value: value, account: account)
        }
    }
    
    // If not present, returns nil. Only throws on error.
    static func get(account: String) throws -> Data? {
        var result: AnyObject?
        let status = SecItemCopyMatching([
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            kSecReturnData: true,
            ] as NSDictionary, &result)
        if status == errSecSuccess {
            return result as? Data
        } else if status == errSecItemNotFound {
            return nil
        } else {
            throw Errors.keychainError
        }
    }
    
    /// Delete a single item.
    static func delete(account: String) throws {
        let status = SecItemDelete([
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            ] as NSDictionary)
        guard status == errSecSuccess else { throw Errors.keychainError }
    }
    
    /// Delete all items for my app. Useful on eg logout.
    static func deleteAll() throws {
        let status = SecItemDelete([
            kSecClass: kSecClassGenericPassword,
            ] as NSDictionary)
        guard status == errSecSuccess else { throw Errors.keychainError }
    }
    
    enum Errors: Error {
        case keychainError
    }
}

Example

And here’s how you’d use it, although you shouldn’t be force-unwrapping things in practice!

try Keychain.set(value: "FooBar".data(using: .utf8)!, account: "username")
try Keychain.set(value: "YadaYada".data(using: .utf8)!, account: "password")
let user = try Keychain.get(account: "username")
let pass = try Keychain.get(account: "password")
try Keychain.delete(account: "username")
try Keychain.deleteAll()

Alternatives

If you want something a bit more thorough and feature-rich, please consider these alternatives which look reasonably good: matthewpalmer/Locksmith and jrendel/SwiftKeychainWrapper

Having said that, I hope this post is still a good example of how to handle C libraries that require UnsafeRawPointers.

MIT license applies. No warranties. You should get a security team to review all code in your project, such as this.

Thanks for reading, I hope this helps someone, and have a great week!

Photo by Chunlea Ju on Unsplash

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

iOS Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS