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.

4.9 (11) · 1 Review

Download materials
Save for later
Share

Apple’s Keychain Services is a mechanism for storing small, sensitive data such as passwords, encryption keys or user tokens in a secure and protected manner. Using Keychain Services, you can check that the password your user is entering matches their stored password without putting data at risk. However, entering a password is tedious! To solve this problem, Apple added biometric authentication to many devices. Biometric authentication allows users to confirm their identity quickly and securely, using either a fingerprint or a face scan.

In this tutorial, you’ll learn how to integrate Keychain Services and biometric authentication into a simple password-protected note-taking SwiftUI app.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Open the starter project. Build and run. On the first run, the app prompts you for a password to protect your note. Go ahead and type a password into the two fields and tap Set Password. You’ll see a simple note-taking app that allows you to enter text and return to it later.

Note: Don’t worry if you forget your password – when running debug builds there’s a magical button to reset the app back to its initial state. You’ll lose your secret note, but you won’t be stuck.

The app wraps a UITextView to manage the note. Above the editor, you’ll see three buttons. On the far left is a trash can button, which is the reset button mentioned above. On the right is one button for changing the password and another for locking and unlocking the note.

Since you just entered a new password, the note is now unlocked and ready for editing. Type some text into the text editor.

Starter App With Text

Now stop and restart the app. The app starts with the note locked. The editor window is blurred to obscure the contents. Tap the lock button and enter the password you set earlier to unlock your note. The app uses User Defaults to store your note and password.

This makes it easy for an attacker with physical access to a device to locate the password! Keychain Services provides a safer place to store the password. In the next section, you’ll begin to update the app to do this.

A Look at Keychain Services

Keychain Services provides an encrypted database managed by the operating system. It is for storing passwords, cryptographic keys, certificates or any short piece of data.

To store something in your keychain, you package several attributes about the data along with the secret information. You store all of them together into a keychain item. Keychain Services provides classes for different types of items:

  • kSecClassInternetPassword stores a password for an internet site.
  • kSecClassGenericPassword stores a password of any type.
  • kSecClassCertificate stores a certificate.
  • kSecClassKey stores a cryptographic key item.
  • kSecClassIdentity stores an identity.

Each class uses a different set of attributes. These attributes define the information needed to identify the secured item. They also control access to the secret information. You can use the attributes to search for the item at a later time. If needed, you can share a keychain among apps.

The Keychain Services API has been around a long time. That means it’s a proven, reliable and safe place to store information. Unfortunately, it also means you’re going to deal with an API written for C! :]

But don’t panic. You’re going to write wrapper functions to let you deal with the API in a more modern fashion.

Enabling Your Keychain

Now it’s time to set up your keychain. You’ll enhance the app to add, retrieve, update, and delete a password… securely!

Adding a Password to the Keychain

In the starter project, open KeychainServices.swift in the Models group. You’ll see the definition for KeychainWrapperError, a custom Error that you’ll use to provide feedback to the user.

The first thing you’ll add is an initial definition for KeychainWrapper. Insert the following code at the end of the file, after KeychainWrapperError:

class KeychainWrapper {
  func storeGenericPasswordFor(
    account: String,
    service: String,
    password: String
  ) throws {
    guard let passwordData = password.data(using: .utf8) else {
      print("Error converting value to data.")
      throw KeychainWrapperError(type: .badData)
    }
  }
}

You must first convert the password from a String to Data. If the conversion fails, you throw an error.

Xcode Warning Defined But Never Used

Don’t worry — you’ll resolve those warnings later in the tutorial. You can ignore them for now.

Note: As you add code, you’ll see warnings about constants being “defined but never used”:

Access to Keychain Services works through a query. The first step in accessing Keychain Services is to create an add query. As the name implies, the add query defines the data you wish to store in the keychain.

Add the following code to the end of storeGenericPasswordFor(account:service:password:):

// 1
let query: [String: Any] = [
  // 2
  kSecClass as String: kSecClassGenericPassword,
  // 3
  kSecAttrAccount as String: account,
  // 4
  kSecAttrService as String: service,
  // 5
  kSecValueData as String: passwordData
]

Here’s what’s happening:

  1. The query is a dictionary that maps a String to an Any object, depending on the attribute. This pattern is common when calling C-based APIs from Swift. For each attribute, you supply the defined global constant beginning with kSec. In each case, you cast the constant to a String (it’s a CFString really), and you follow it with the value for that attribute.
  2. The fist key defines the class for this item as a generic password, using the pre-defined constant kSecClassGenericPassword.
  3. For a generic password item, you provide an account, which is your username field. You passed this into the method as a parameter.
  4. Next, you set the service for the password. This is an arbitrary string that should reflect the purpose of the password, for example, “user login”. You also passed this into the method as a parameter.
  5. Finally, you set the data for the item using the passwordData converted from the string passed into the method.

Now that you’ve built the query, you’re ready to store the value. Add the following code after the query definition:

// 1
let status = SecItemAdd(query as CFDictionary, nil)
// 2
switch status {
// 3
case errSecSuccess:
  break
// 4
default:
  throw KeychainWrapperError(status: status, type: .servicesError)
}

Here’s what this code is doing:

  1. SecItemAdd(_:_:) asks Keychain Services to add information to the keychain. You cast the query to the expected CFDictionary type. C APIs often use the return value to show the result of a function. Here the value has type OSStatus.
  2. You switch on the various values of the status code. It might seem odd to use a switch where you’re only checking one value, but who knows what might happen in the future ;]
  3. errSecSuccess means your password is now in the keychain. Your work here is done!
  4. If status contains another value, the function failed. KeychainWrapperError includes an initializer which uses SecCopyErrorMessageString(_:_:) to create a human-readable message for the exception.

You use this same pattern to access all Keychain capabilities: First, you create a query defining the work to do, and then you call a function with that query.

You now have a method to store a password in the keychain. Next, you’ll add search functionality to find and retrieve the item you just added.