How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI
Learn how to integrate keychain services and biometric authentication into a simple password-protected note-taking SwiftUI app. By Bill Morefield.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI
30 mins
- Getting Started
- A Look at Keychain Services
- Enabling Your Keychain
- Adding a Password to the Keychain
- Searching for Keychain Items
- Using Keychain Services in SwiftUI
- Updating a Password in the Keychain
- Deleting a Password From the Keychain
- Biometric Authentication in SwiftUI
- Enabling Biometric Authentication in Your App
- Simulating Biometric Authentication in Xcode
- Simulating Touch ID Authentication
- Simulating Face ID Authentication
- Making the Authentication Method Visible to the User
- Where to go from here?
Searching for Keychain Items
The steps to read an item from the keychain mirror those to add the item. Add the following new method to the end of the KeychainWrapper
class:
func getGenericPasswordFor(account: String, service: String) throws -> String {
let query: [String: Any] = [
// 1
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
// 2
kSecMatchLimit as String: kSecMatchLimitOne,
// 3
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
}
Again, the first step when reading an item from the keychain is to create the appropriate query:
- You supplied
kSecClass
,kSecAttrAccount
andkSecAttrService
when adding the password to the keychain. You can now use these values to find the item in the keychain. - You use
kSecMatchLimit
to tell Keychain Services you expect a single item as a search result. - The final two parameters in the dictionary tell Keychain Services to return all data and attributes for the found value.
After the query, add the following code:
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
throw KeychainWrapperError(type: .itemNotFound)
}
guard status == errSecSuccess else {
throw KeychainWrapperError(status: status, type: .servicesError)
}
First, you define an optional CFTypeRef
variable to hold the value Keychain Services will hopefully find. You then call SecItemCopyMatching(_:_:)
. You provide the query and a pointer to the destination value. This function searches the keychain for a match and copies the match to item
.
Again, the status code provides error information. This code contains a specific error when Keychain Services doesn’t find the requested item.
Now you have the keychain item, but as a CFTypeRef
. Add the following code at the end of getGenericPasswordFor(account:service:)
:
// 1
guard
let existingItem = item as? [String: Any],
// 2
let valueData = existingItem[kSecValueData as String] as? Data,
// 3
let value = String(data: valueData, encoding: .utf8)
else {
// 4
throw KeychainWrapperError(type: .unableToConvertToString)
}
// 5
return value
Here’s what’s happening:
- Cast the returned
CFTypeRef
to a dictionary. - Extract the
kSecValueData
value in the dictionary and cast it toData
. - Attempt to convert the data back to a string, reversing what you did when storing the password.
- If any of these steps return
nil
, this means the data couldn’t be read. You throw an error. - If the casts and conversions succeed, you return the string containing the stored password.
Now you’ve implemented all the capabilities needed to store and retrieve a keychain item. Next, you’ll hook up your user interface so you can see it work!
Using Keychain Services in SwiftUI
Open NoteData.swift under the Models group. Access to the password from the app uses two methods: getStoredPassword()
reads the password, and updateStoredPassword(_:)
sets the password. There’s also a property, isPasswordBlank
.
First, delete the statement let passwordKey = "Password"
at the start of the class.
Now replace the existing getStoredPassword()
method with this:
func getStoredPassword() -> String {
let kcw = KeychainWrapper()
if let password = try? kcw.getGenericPasswordFor(
account: "RWQuickNote",
service: "unlockPassword") {
return password
}
return ""
}
This method creates a KeychainWrapper
and calls getGenericPasswordFor(account:service:)
to read the password and return it. The try?
expression converts an exception to a nil. This will cause the method to return an empty string if the search was unsuccessful.
Next, replace updateStoredPassword(_:)
with this:
func updateStoredPassword(_ password: String) {
let kcw = KeychainWrapper()
do {
try kcw.storeGenericPasswordFor(
account: "RWQuickNote",
service: "unlockPassword",
password: password)
} catch let error as KeychainWrapperError {
print("Exception setting password: \(error.message ?? "no message")")
} catch {
print("An error occurred setting the password.")
}
}
You use KeychainWrapper
to set a password using the same account
and service
. For this app, you’ll print any errors to the console.
Now build and run. The app again asks you to set a password when the app runs. But your app no longer reads from UserDefaults, which is not secure. Instead, it uses the encrypted keychain to store and retrieve the password.
Enter a new password, and you’ll see the note from your earlier run. Tap the lock button twice. This locks and then unlocks the note, using the password you just set. Your password works, and the note appears!
You can now add a password to your keychain, and you can retrieve it to authenticate the user. But you still need to add a few more methods to complete the implementation of Keychain Services in your app.
Updating a Password in the Keychain
With the app running and the note unlocked, tap the double-arrow button to change the password. Enter a new password and tap Set Password. An error appears in the console window:
You can’t add something to the keychain if the item already exists! Instead, you need to update the stored item.
Open KeychainServices.swift. Add the following code at the end of your KeychainWrapper
class:
func updateGenericPasswordFor(
account: String,
service: String,
password: String
) throws {
guard let passwordData = password.data(using: .utf8) else {
print("Error converting value to data.")
return
}
// 1
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service
]
// 2
let attributes: [String: Any] = [
kSecValueData as String: passwordData
]
// 3
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
throw KeychainWrapperError(
message: "Matching Item Not Found",
type: .itemNotFound)
}
guard status == errSecSuccess else {
throw KeychainWrapperError(status: status, type: .servicesError)
}
}
This code is similar to storeGenericPasswordFor(account:service:password:)
, which you added earlier. An update, however, requires two dictionaries, one with the search query and one with the desired changes. Here’s the breakdown:
- The search query specifies the data you want to update. You provide the attributes as you did with the search query you created earlier, but you don’t use search parameters such as match limit and return attributes. The function will update all matching entries.
- The second dictionary contains the data to update. You may specify any or all attributes valid for the class, but you only include the ones you want to change. Here you specify only the new password. But you could also set
service
oraccount
if you wanted to store new values for those attributes. -
SecItemUpdate(_:_:)
uses the contents of the two dictionaries above and performs the update. The most common error you’ll see is The specified attribute does not exist. This error indicates that Keychain Services found nothing matching the search query.
You don’t want to have to keep checking and deciding if you need to write a new keychain item or update an existing one, or dealing with the errors that come up if you call the wrong method. You’re going to fix that now.
In storeGenericPasswordFor(account:service:password:)
, find the switch status
statement and add a new case above the default:
case errSecDuplicateItem:
try updateGenericPasswordFor(
account: account,
service: service,
password: password)
errSecDuplicateItem
is the status returned when storing an existing item. Now, if you attempt to store an existing item, you’ll fall back to updating it instead.
Build and run. Use the password you set earlier to unlock the note. (Remember that your password change, above, did not complete. The attempt to add an item that already existed caused an exception.) Try again to change the password — success!
Now your app can add, retrieve, and update a password. But there’s still one action missing. You need to be able to delete a value from the keychain.