If you’ve ever built an iOS app, you’ve probably come across a situation where you need to store sensitive information on behalf of the user. For this, ...

If you’ve ever built an iOS app, you’ve probably come across a situation where you need to store sensitive information on behalf of the user. For this, Apple’s Keychain Services is your guy; it’s good for saving secure data. UserDefaults are fine, when you’re dealing with preferences, but you should never store credentials in them. The keychain is not limited to passwords. You can store other secrets that the user explicitly cares about, such as credit card information or even short notes. You can also store items that the user needs but may not be aware of, for example, the cryptographic keys and certificates that you manage. You can use the keychain to store these items as well. Keep in mind that Keychain is stored in a secure enclave in the A7 or newest chips. There are many wrappers where you can easily use Keychain, but I want to show that Keychain is not difficult and we can use it as well.
Cryptographic keys are strings of bytes that you can combine with other data in specialized mathematical operations to enhance security. When you generate keys yourself, you can store them in keychain. To do this, begin by creating a query dictionary containing and describing the item. To make our codebase cleaner and more maintainable, let’s create WritableKeychain protocol with prepareKey method and using extension of this protocol write simple implementation storing keys in keychain like this:
enum KeychainError: Error {
case unhandledError(status: OSStatus)
}
protocol WritableKeychain {
func prepareKey(_ key: String) throws
}
extension WritableKeychain {
func prepareKey(_ key: String) throws {
guard let keyData = key.data(using: .utf8) else {
fatalError("Key can't be convert to expected object")
}
guard let tag = "com.example.key1".data(using: .utf8) else {
fatalError("Tag can't be convert to expected object")
}
// 1
let attributes: [String: Any] = [kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecValueData as String: keyData]
// 2
_ = SecItemDelete(attributes as CFDictionary)
// 3
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
}
}
Here’s what’s happening in the code:
When you want to retrieve the key, you have to make another query using the same application tag. To handle this let’s make another protocol:
protocol ReadableKeychain {
var key: String { get }
}
extension ReadableKeychain {
var key: String {
guard let tag = "com.example.key1".data(using: .utf8) else {
fatalError("Tag can't be convert to expected object")
}
// 1
let attributes: [String: Any] = [kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecReturnData as String: true]
// 2
var item: CFTypeRef?
let status = SecItemCopyMatching(attributes as CFDictionary, &item)
guard status == errSecSuccess else {
fatalError("Cannot find saved key in Keychain")
}
// 3
guard let keyData = item as? Data else {
fatalError("Key was found, but can't be convert to expected object ")
}
let value = String(data: keyData, encoding: .utf8)!
return value
}
}
Let’s take a look at the code:
That’s it. Let’s make simple manager to create and retrieve keys for our purposes :)
typealias Keychain = WritableKeychain & ReadableKeychain
class KeychainManager: Keychain {
func prepare() {
do {
try prepareKey("something_to_encode")
} catch let error {
print(error.localizedDescription)
}
}
}
let manager = KeychainManager()
manager.prepare()
print(manager.key)
The Keychain Service lets us to enable simple, secure storage for users password. In this way you avoid repeatedly asking the user for a password, and you don’t have to implement your own encryption, which can be prone to errors. To get started, create a structure to hold user credentials:
struct Credentials {
var username: String
var password: String
}
Also identify the server that your app is working with:
struct Constants {
static let server = "www.yourserver.com"
}
Next we can extend our Writable protocol with an additional method;
protocol WritableKeychain {
func prepareKey(_ key: String) throws
func saveUserCredentials(credentials: Credentials) throws
}
and create an implementation to store those credentials in Keychain.
extension WritableKeychain {
// ...
func saveUserCredentials(credentials: Credentials) throws {
let username = credentials.username
guard let password = credentials.password.data(using: .utf8) else {
fatalError("Password value can't be convert to expected object")
}
// 1
let attributes: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: Constants.server,
kSecValueData as String: password]
// 2
_ = SecItemDelete(attributes as CFDictionary)
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
}
}
Let’s see what happened:
Note
Keychain service also provides us kSecClassGenericPassword item class. Generic passwords are similar to Internet passwords, but they lack certain attributes specific to remote access (for example don’t have to have kSecAttrServer attribute). If you don’t need extra attributes, user generic password instead.
Sometimes your users can login to your app with different accounts on one device. Before saving new credentials to Keychain, please delete the previous entries.
Our user credentials are now saved in Keychain and protected; Great! :)
Let’s retrieve those values back;
Expand Readable protocol with new property and make implementation:
protocol ReadableKeychain {
var key: String { get }
var credentials: Credentials { get }
}
extension ReadableKeychain {
// ...
var credentials: Credentials {
// 1
let attributes: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: Constants.server,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
// 2
var item: CFTypeRef?
let status = SecItemCopyMatching(attributes as CFDictionary, &item)
guard status != errSecItemNotFound else { fatalError("Item not found") }
guard status == errSecSuccess else { fatalError("Cannot find user credentials in Keychain") }
// 3
guard let existingItem = item as? [String: Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: .utf8),
let account = existingItem[kSecAttrAccount as String] as? String else {
fatalError("Unexpected password data")
}
return Credentials(username: account, password: password)
}
}
Some explanations:
That’s it. Let’s expand our previous manager with this additional method to create and retrieve user credentials.
class KeychainManager: Keychain {
// ...
func prepareCredentials() {
let credentials = Credentials(username: "your_username", password: "your_password")
do {
try manager.saveUserCredentials(credentials: credentials)
} catch let error {
print(error.localizedDescription)
}
}
}
Our method call
manager.prepareCredentials()
let credentials = manager.credentials
print("username: \(credentials.username) and password: \(credentials.password)")
A good understanding of Keychain let us realize how implementing your wrapper can be pretty easy and beneficial to users and securing their credentials. You can avoid extra third-party libraries and speed up the build time as well as reduce app size. I’ll tell you more about security in iOS in upcoming blog posts.
The eCommerce agency behind 100+ high-stakes projects, ready to build yours.