The secure way to store secrets on iOS devices

Where to store secrets on iOS devices? In this article, we will go through available methods and show you the recommended way to do this.

Wojciech Reguła 2021.04.15   –   11 MIN read
The secure way to store secrets on iOS devices

TL;DR

  • Whenever possible, avoid storing secrets on the device. 
  • Keychain is the right place to store your small app’s secrets.
  • Entries saved in the Keychain can be additionally protected by setting proper accessibility and authentication flags.
  • Watch out what you synchronize with iCloud.
  • Files stored in the application container can also be additionally protected.
  • Always follow good mobile application development practices -> see our Guidelines on mobile application security – iOS edition.

Background 

During the last few years, while pentesting iOS apps, I observed a lot of bad secret storage patterns. From a security perspective there is a recommended approach, but, before you start saving a secret on the device we need to make up if that is even necessary. The general idea is not to store sensitive information if you don’t have to. If you made that decision there are typically 2 kinds of secrets: small ones and the big ones. Small secrets can be for example session tokens, encryption keys, certificates. Big secrets are databases, videos, pictures. Below, I will show you a secure approach to store such data.

A safe place for small secrets

iOS Keychain is considered the best place to store your application’s small secrets. The Keychain is encrypted using a combination of Device Key and user passcode (if set). Your application will talk to security in order to interact with the SQLite database containing the encrypted secrets. The Keychain facility gives us features that help with restricting access to the entries, applying additional authentication policies, synchronizing the entries with iCloud, or even giving access to our entries to other apps.

iOS gives us a SecItem* C API to manage the secrets. From my experience, developers usually don’t use it directly as this API is a bit complicated. Instead, devs use different wrappers. In the example, I will use https://github.com/kishikawakatsumi/KeychainAccess

Let’s take a look at the code snippet below:

import KeychainAccess
func saveSecureKeychainItem() {
let keychain = Keychain(service: "example-service")
DispatchQueue.global().async {
   do {
       try keychain
       .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .biometryCurrentSet)
       .synchronizable(false)
       .set("secure-entry-value", key: "secure-entry-key")
   } catch let error {
       // error handling
   }
}
}

I defined a service name and added the entry to the Keychain. I also set the secure attributes for that entry making sure that:

  • It will be accessible only when the device is unlocked,
  • It will be saved only when the user set a password,
  • It will never leave the device,
  • It will never be synchronized,
  • It will require the user’s presence to be obtained,
  • It will be invalidated if the user changes anything in the biometry settings (for example enrolling a new Face to FaceID).

By default, most of the frameworks / keychain wrappers will set minimal security constraints. However, I always recommend overriding the default values to be sure that the secret will be stored as expected. Implementation changes and thus the default settings change as well.

For the typical secrets, you won’t need that additional protections, such as requiring the user’s presence. The typical implementation looks as follows:

import KeychainAccess
func saveLessSecureKeychainItem() {
let keychain = Keychain(service: "example-service")
DispatchQueue.global().async {
   do {
       try keychain
       .accessibility(.afterFirstUnlockThisDeviceOnly)
       .synchronizable(false)
       .set("secure-entry-value", key: "secure-entry-key")
   } catch let error {
       // error handling
   }
}
}

I strongly encourage you to read more about the available secure flags. Look at the links below:

👉 Restricting Keychain Item Accessibility
👉 Accessing Keychain Items with Face ID or Touch ID
👉 kSecAttrSynchronizable

Where to store big secrets?

Big secrets are stored within your application’s container as regular files. By default, all the files are encrypted and cannot be accessed before the first unlock. So, any application (after the first device unlock) that can escape the sandbox will be able to get your big secrets. This of course requires exploiting a vulnerability in iOS but this article is about hardening. Under certain circumstances applications that are run on jailbroken devices can also escape their containers without any additional vulnerabilities.

The mechanism that allows us to control the on-disk files encryption is called Data Protection API. If you want your file to be accessible only when the device is unlocked, it means you want to use the NSFileProtectionComplete flag. Below you can see the example:

do {
  try data.write(to: fileURL, options: .completeFileProtection)
}
catch let error {
   // error handling
}

OK, but what if your threat model requires an additional layer of protection? If you don’t want to be your big secret accessible to the sandbox escaper? Well, let’s take a look at the example below:

import GRDB
func openDB() -> DatabaseQueue? {
   do {
       var config = Configuration()
       config.prepareDatabase = { db in
           try db.usePassphrase(getDatabasePassphraseFromKeychain())
       }
       let dbQueue = try DatabaseQueue(path: getDBPath(), configuration: config)
       return dbQueue
   } catch let error {
       // error handling
   }
   return nil
}

I used GRDB to create an SQLite database. What’s special about this database is that it’s encrypted using SQLCipher. The passphrase is stored securely in the Keychain. Please keep in mind that there are 2 traps. The first one is about the passphrase storage – do not hardcode it in your code! The second trap is managing the passphrase in-memory lifetime. As you can see in the example above, I obtain the secret in the closure to create the Configuration() that is then used to open the database. The passphrase is not stored in the memory longer than necessary.

Summary

Do not store secrets on the device if you do not necessarily have to. That’s the sentence I want to repeat once again in the summary. My iOS apps pentesting experience shows that secrets are often stored insecurely. I saw them in Info.plist files or even hardcoded. In this article I wanted to show you how to properly store secrets that have to be on the device. Use Keychain with proper security flags in order to store small secrets. Consider additionally encrypting big secrets with the properly stored encryption key.

It is also always worth following good practices in the iOS application development process. In this topic, we especially recommend our own guide, where we have gathered our experience with ready-made solutions.

If you have any questions, feel free to use our contact form, or reach directly to me on Twitter on LinkedIn

Wojciech Reguła
Wojciech Reguła Principal IT Security Consultant
Head of Mobile Security