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.
myString.data(using: .utf8)
and String(data: keychainData, encoding: .utf8)
when calling it with string data.kSecAttrAccessibleAfterFirstUnlock
.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
instead.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
}
}
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()
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.
(Comp Sci, Hons - UTS)
Software 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