Introducing CryptoKit

Cryptography is key to protecting your users’ data. This tutorial shows you how to use Apple’s new CryptoKit API to authenticate or encrypt your app’s data. By Audrey Tam.

Leave a rating/review
Download materials
Save for later
Share

Apple’s new CryptoKit API provides you the ability to authenticate and encrypt data sent and received by your app, if your app needs more than Apple’s default hardware and software protection.

In this article, you’ll authenticate and encrypt data with both symmetric key and public key cryptography. You’ll learn when and how to use:

  • Cryptographic hashing
  • Hash-based Message Authentication Code (HMAC)
  • Authenticated Encryption with Associated Data (AEAD) ciphers
  • Public key cryptography with elliptic curves (ECC)
  • Digital signatures
  • Key agreement

Keep reading to unlock all the secrets!

Spoiler Alert: This tutorial assumes you’re familiar with the Harry Potter series by J.K. Rowling or have no interest in reading it. It discusses secrets revealed in the last few books.

Getting Started

Get started by downloading the materials for this article — you can find the link at the top or bottom of this article. Open, build and run the SecretKeeper project in the starter folder.

SecretKeeper app

SecretKeeper app

SecretKeeper app

This app is a list of Voldemort’s horcruxes. As long as part of his soul is safe in one of these items, Voldemort cannot be killed. So he must keep the secret of their identities and locations from everyone, especially Albus Dumbledore and Harry Potter.

Protecting Users’ Data

Like Voldemort’s secrets, your users’ data needs protection from accidental or malicious corruption and from unauthorized use. iOS already has a lot of built-in or easy-to-use security in Foundation/NSData, Security/Keychain and CloudKit. You get TLS encryption and digital signing for free if your app accesses HTTPS servers. And now there’s CryptoKit, if your app needs to verify data or authenticate senders.

You need to protect data in each of these three states:

  • Data in motion: in transit, usually over a network
  • Data in use: in memory
  • >Data at rest: stored on disk, not in use

Data in motion is the original reason for encryption, going back to before the Caesar cipher for sending written orders to Caesar’s generals. Nowadays, encryption is handled by Transport Layer Security (TLS) if your app communicates with a server using TLS 1.3. But you might still need to authenticate the data and sender with cryptographic hashing and keyed signatures. Most of this tutorial is about these topics.

Data in use must be protected by user authentication. To restrict when an operation can be executed, use Local Authentication to check when and how the user authenticated. For example, you could require the user to authenticate with biometrics — Face ID or Touch ID — to ensure the user is present.

Data at rest is protected on the device by the Data Protection API. Users get automatic software data protection as soon as they set a passcode for the device. Every new file gets a new 256-bit encryption key, specific to the file’s protection type. There are three file protection types:

  • Protected Until First User Authentication: The default file protection type. The file is not accessible while the device is booting up, but is then accessible until the device reboots, even while the device is locked.
  • Complete: The file is accessible only when the device is unlocked.
  • Complete Unless Open: If the file is open when the device locks, it’s still accessible. If the file is not open when the device locks, it’s not accessible until the device unlocks.

You might want to allow access to a file when the device is locked to enable network transfers to continue.

Your app can also store encrypted data in a private CloudKit database, where it’s available on all the user’s devices.

Increasing Protection for Voldemort’s Secrets

SecretKeeper reads from a file to create the list of horcruxes. The app writes this file with no file protection option:

try data.write(to: secretsURL)

This means the secrets file has only the default file protection, so it’s accessible when the app is in the background and even when the device is locked.

Voldemort’s secrets need a higher level of file protection. In ContentView.swift, in writeFile(items:), replace the data.write(to:) statement with this line:

try data.write(to: secretsURL, options: .completeFileProtection)

With this level of protection, the file is accessible only when the device is unlocked.

Keychain and Secure Enclave

Your app should store encryption keys, authentication tokens and passwords in the local or iCloud Keychain. Apple’s sample app Storing CryptoKit Keys in the Keychain shows how to convert CryptoKit key types P256, P384 and P521 to SecKey and other key types to generic passwords.

iOS devices released since late 2013 — iPhone 5S, iPad Air and later models — have the Secure Enclave chip. The Secure Enclave is a dedicated cryptography engine, separate from the processor, with the device’s Unique IDentifier (UID) 256-bit encryption key fused in at the factory. The Secure Enclave also stores biometric data for TouchID and FaceID.

Keys and data in the Secure Enclave never leave it. They’re never loaded into memory or written to disk, so they’re completely protected. Your app communicates with the Secure Enclave via a mailbox, where you deposit data to be encrypted or decrypted, then retrieve the results.

CryptoKit enables your app to create a key for public-key cryptography directly in the Secure Enclave. There’s sample code near the end of this tutorial.

Using CryptoKit

All this built-in data protection might be as much as your app needs. Or, you might need to use CryptoKit if your app works with data encrypted by third parties or authenticates file transfers or financial transactions.

Rolling Your Own Crypto: Don’t

Data security is a battleground between cryptographers and attackers. Unless you’re a cryptographer, writing your own cryptographic algorithms is a huge risk for your users’ data. There are many cryptography frameworks for different platforms. And now you have Apple’s CryptoKit making it super easy to use cryptography in your apps.

Performance: Don’t Worry

What about performance? CryptoKit is built on top of corecrypto, Apple’s native cryptographic library. Corecrypto’s hand-tuned assembly code makes highly efficient use of the hardware.

Cryptographic Hashing

Now for the most basic form of cryptography. You’re probably familiar with the idea of hashing dictionary items or database records for more efficient retrieval. Many standard types conform to the Hashable protocol. Hashing objects of these types into a Hasher produces integer hash values with a reasonable probability of uniqueness. A small change to the input produces a large change in the hash value.

Hasher uses a randomly generated seed value, so it produces different hash values each time your app runs. See this for yourself.

Open CryptoKitTutorial.playground in the starter folder. The first section, Hashable Protocol, has this hashItem(item:) function:

func hashItem(item: String) -> Int {
  var hasher = Hasher()
  item.hash(into: &hasher)
  return hasher.finalize()
}

Try it out by adding this line below it:

let hashValue = hashItem(item: "the quick brown fox")

Run the playground. The hash value appears in the results sidebar, or click the Show Result button to view it in the playground:

Hasher hash value

Hasher hash value

Hasher hash value

Your value is different from mine. Run the playground again, and you’ll get a different value.

Unlike Hasher, cryptographic hashing produces the same hash value every time. Like the Hasher algorithm, cryptographic hashing algorithms produce almost-unique hash values, and a small change to the input produces a large change in the hash value. The difference is one of degree and the amount of computation required to produce a hash value — called a digest or checksum — that can reliably verify the integrity of data.

Cryptographic hashing algorithms create a small, fixed length, almost unique data value (digest) for the input data. The most common digest sizes are 256 and 512 bits. The 512-bit digest can be truncated to 384 bits. Finding two inputs that produce the same digest is very unlikely but not impossible, as there are, respectively, only 2256 or 2512 possible values for a 256-bit or 512-bit digest.

Hashing algorithms are one-way and include non-linear operations, so attackers can’t reverse the operations to compute the original data from the digest. Each bit of the output is dependent on every bit of the input so attackers can’t try to compute part of the input from part of the digest. Changing even a single bit in the input produces a completely different digest, so attackers can’t find relationships between inputs and outputs.

Cryptographic digest is almost unique and very hard to reverse.

Cryptographic digest is almost unique and very hard to reverse.

Cryptographic digest is almost unique and very hard to reverse.

These characteristics let you see if two data sets are different by computing digests. For example, Git computes a digest to identify every commit. You can transmit or store data with its digest to detect subsequent changes like data corruption. Software downloads often provide a digest, called a checksum. The downloader uses the same hashing algorithm to compute a digest for the downloaded data. If this digest doesn’t match the file’s checksum, the file is corrupted or incomplete.

Receiver computes digest to check data is complete, not corrupted.

Receiver computes digest to check data is complete, not corrupted.

Receiver computes digest to check data is complete, not corrupted.

CryptoKit provides the Secure Hash Algorithm 2 (SHA-2) algorithms SHA-256, SHA-384 and SHA-512. The numbers indicate the digest size. Its Insecure container provides SHA-1 (160-bit) and MD5 (128-bit) for backward compatibility with older services. SHA-2 was published by the U.S. National Institute of Standards and Technology (NIST) and is required by law for use in certain U.S. Government applications.

In the playground, scroll down to this statement:

UIImage(data: data)

In the results sidebar, click Show Result next to w 714 h 900 to see the data is an image of the one-year-old Harry Potter:

Data is baby Harry Potter image.

Data is baby Harry Potter image.

Data is baby Harry Potter image.

When Voldemort failed to kill Harry, he accidentally turned him into a horcrux. But Voldemort doesn’t know this. Albus Dumbledore knows, and he wants to share this secret with Harry.

First you, as Dumbledore, create a digest of this data. Add this line:

let digest = SHA256.hash(data: data)

That’s all you have to do! SHA256 produces a 256-bit digest. It’s just as easy to use one of the other two hashing algorithms — SHA384 or SHA512. And if Apple updates these to use SHA-3 instead of SHA-2, you won’t have to change your code.

Then Dumbledore sends the data and digest over a network connection to Harry. So now, as Harry, add this code to check the digests match:

let receivedDataDigest = SHA256.hash(data: data)
if digest == receivedDataDigest { 
  print("Data received == data sent.") 
}

You use the same hashing algorithm on the data then check the two digest values are equal.

Run the playground. The message prints. Not surprising, as data had no opportunity to become corrupted.

Now click the Show Result button next to receivedDataDigest to get a good look at the digest:

CryptoKit SHA256 digest of data

CryptoKit SHA256 digest of data

CryptoKit SHA256 digest of data

It’s a tuple. Not very readable.

Type this line to show its description instead:

String(describing: digest)
Note: Apple’s documentation discourages calling .description directly.

Run the playground. Click Show Result:

String description of CryptoKit digest

String description of CryptoKit digest

String description of CryptoKit digest

The digest description contains a String. This is more human-readable and looks like a Git hash value, but twice as long. It’s also much much much longer than the Hasher hash value.

Finally, to see how a small change in data produces a completely different digest, make a very small change to one of the two identical lines of code. For example:

String(describing: SHA256.hash(data: "Harry is a horcrux".data(using: .utf8)!))
String(describing: SHA256.hash(data: "Harry's a horcrux".data(using: .utf8)!))

Run the playground. Click Show Result for both lines:

CryptoKit digests of almost-identical data are very different.

CryptoKit digests of almost-identical data are very different.

CryptoKit digests of almost-identical data are very different.

Notice you’re getting exactly the same hash values I did.