One of the great hardware features of iPhone is Secure Enclave — a special hardware element designed to protect user’s sensitive data, including biometric data like fingerprints when device has a Touch ID sensor and face scans in case of Face ID. Secure Enclave ensures that this kind of data is safe even if a hacker gets access to device RAM or disk storage — the thing is that this data never gets to RAM and is never processed by OS or by programs in user space. OS or a program can only interact with Secure Enclave hardware using a predefined set of commands which doesn’t allow access to raw data.
Besides biometric data, Secure Enclave can store cryptographic keys. Such keys are generated inside this hardware element and never “leave” it — there’s no way to get them (unless you break somehow the hardware protection — as for now, there were no reports, suggesting that this is possible).
Currently, only Elliptic-curve cryptography keys can be stored in Secure Enclave. This is an asymmetric cryptography approach so we can talk about public and private keys here. The private key is generated and stored in Secure Enclave. The corresponding public key is available for export and can be transmitted to a communication counterparty or used for encryption locally.
Generating and fetching a key
To generate a Secure Enclave-stored key we can use a SecKeyCreateRandomKey
call with special attributes. Here’s how this can be done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
static func makeAndStoreKey(name: String, requiresBiometry: Bool = false) throws -> SecKey { let flags: SecAccessControlCreateFlags if #available(iOS 11.3, *) { flags = requiresBiometry ? [.privateKeyUsage, .biometryCurrentSet] : .privateKeyUsage } else { flags = requiresBiometry ? [.privateKeyUsage, .touchIDCurrentSet] : .privateKeyUsage } let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, nil)! let tag = name.data(using: .utf8)! let attributes: [String: Any] = [ kSecAttrKeyType as String : kSecAttrKeyTypeEC, kSecAttrKeySizeInBits as String : 256, kSecAttrTokenID as String : kSecAttrTokenIDSecureEnclave, kSecPrivateKeyAttrs as String : [ kSecAttrIsPermanent as String : true, kSecAttrApplicationTag as String : tag, kSecAttrAccessControl as String : access ] ] var error: Unmanaged<CFError>? guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { throw error!.takeRetainedValue() as Error } return privateKey } |
The key type is set to kSecAttrKeyTypeEC
and its size is indicated as 256 bits — only this kind of keys can be stored in Secure Enclave presently.
We can additionally use biometry to protect the key. This means that in order to use it, user will be required to go through Touch ID or Face ID authentication (see my previous post about biometry-protected keychain entries). In the fragment of code above we have a requiresBiometry
parameter which being set to true enables such protection. This is done by setting the .biometryCurrentSet
flag when we create the SecAccessControl
instance that we later use to create the key.
Note that at the end of this procedure we’re getting a SecKey
instance that we later can use in our program. This, however, doesn't mean that the private key we just created is loaded into RAM. Our SecKey
is just a handle object allowing access to the key stored in Secure Enclave.
When we have our key in keychain, we can always load it using a function like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static func loadKey(name: String) -> SecKey? { let tag = name.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String : kSecClassKey, kSecAttrApplicationTag as String : tag, kSecAttrKeyType as String : kSecAttrKeyTypeEC, kSecReturnRef as String : true ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess else { return nil } return (item as! SecKey) } |
We’re getting the key calling the SecItemCopyMatching
function and using the same “application tag” value (the kSecAttrApplicationTag
field) that we indicated at the key creation stage.
Encryption and decryption algorithm
Now let’s look how we can use the key that we just created. Before we proceed we have to decide what encryption algorithm we’re going to use and check that our current device/OS supports it.
In the official documentation Apple suggests using the following algorithm: eciesEncryptionCofactorX963SHA256AESGCM
. This long name basically means that we are usign Elliptic Curve Integrated Encryption Scheme. It refers to the ANSI X9.63 standard. A key derivation function defined in that standard (with SHA-256 hash) is used to generate an encryption key that’s finally used to encrypt the data using the AES-GCM algorithm. The “cofactor” term in the name of the algorithm tells us that some additional measures are taken for protection from certain types of attacks (we won’t delve into details here).
If we take a look at the corresponding constant kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM
defined in SecKey.h (see here for example) then we can see that this algorithm is considered “legacy” and the recommended one is kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM
instead (in Swift it’s eciesEncryptionCofactorVariableIVX963SHA256AESGCM
). We’ll use this recommendation in our example. The only difference between these two algorithms is that in the old algorithm an all-zero initialization vector was used to perform AES-GCM encryption. In the newer algorithm the IV is generated together with shared encryption key during the ECDH procedure (see below).
ECIES and ECDH overview
All this sounds a bit scary so let’s first look at the way ECIES works in general. As we’re talking about cryptography, of course we’ll be talking about Alice and Bob. Suppose Bob wants to send an encrypted message to Alice. The following diagram describes how Alice and Bob can in a safe way agree on a shared key that later can be used for encryption by both of them. Here we omit all the technical and mathematical details to make it as simple as possible.
Elliptic-curve Diffie–Hellman (ECDH)
Here’s what happens on this diagram:
- First Bob gets Alice’s public key.
- Then Bob generates a random value. This random value is also referred as an “ephemeral private key”. “Ephemeral” means that this key will be used only once — for this encryption session only.
- After that Bob generates a public key for his ephemeral key and sends that to Alice
- Now Bob generates a symmetrical key that can be used for encryption. For this he uses Alice’s public key and his own ephemeral private key.
- On the other side Alice can reconstruct the same key using her private key and Bob’s ephemeral public key
So both sides have the same key and Bob can successfully encrypt his message and send it to Alice and she won’t have any problems decrypting it.
This is all nice but how is this related to our case with a key stored in Secure Enclave? We don’t have any network communications in our basic scenario. We just want to encrypt some data. Who is playing the role of Alice and who is playing the role of Bob? We’ll try to answer these questions in the next section.
How does it work in Apple’s crypto-APIs?
Sadly, Apple documentation is quite far from being full and detailed when it comes to this topic. So it takes a bit of guess work and reverse engineering to fully understand what exactly happens when you use these APIs. I can recommend this great post by David Schuetz for detailed description of this matter. It will be especially useful if you’re thinking about some encryption data interoperability (with your backend or other systems).
Here we will focus mostly on the practical side of the problem omiting some low level details. We have already seen how to generate a private key that’s stored in Secure Enclave (makeAndStoreKey
method described above). Now let’s look how we can obtain the corresponding public key and how we can check that a given encryption algorithm is supported by the system.
If we have a key returned by the makeAndStoreKey
method, then getting the corresponding public key is as easy as calling SecKeyCopyPublicKey
and passing the original key as parameter.
Once we have our keys we can check if the algorithm we want to use is supported by the system. Here’s a fragment of code where we get our public key and check that we can encrypt data with it using the eciesEncryptionCofactorVariableIVX963SHA256AESGCM
algorithm:
1 2 3 4 5 6 7 8 9 10 |
guard let publicKey = SecKeyCopyPublicKey(key) else { // Can't get public key return } let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else { // Algorith not supported return } // Now we're ready to encrypt data using publicKey |
As far as I know it will be supported on all iOS 10+ devices with Secure Enclave, but anyway it won’t hurt you to have such checks in your code.
As you can see SecKeyIsAlgorithmSupported
has three parameters: key, operation and algorithm. And all the three parameters are important. You can have different keys, and for each key type different algorithms and operations can be supported. If you experiment a little bit with the code above, you’ll see that if you change the operation to .decrypt
, the method will return false
. At the same time, if you try it with our private key, then you’ll get true
for decryption and false
for encryption.
With this information let’s look at our ECIES scheme again. We know that our private key is stored in Secure Enclave, so in our scenario it is Alice who owns (and uses for decryption) the private key, and it is Bob who uses Alice’s public key to encrypt the message.
But what about the ephemeral key? It turns out that it’s generated every time some data is encrypted. And Bob puts the ephemeral public key together with the ciphertext he generates. Then Alice can pick it up, reconstruct the shared key and decrypt the data.
In Apple’s crypto API SecKeyCreateEncryptedData
is used to encrypt data and SecKeyCreateDecryptedData
is used for decryption. When Bob will call SecKeyCreateEncryptedData
, this function will return a CFData
object containing the ephemeral public key (generated and used during this particular call) and the actual ciphertext. Now Alice can call SecKeyCreateDecryptedData
to decrypt this data. All she needs is her key in Secure Enclave and the ephemeral public key that comes with the data from Bob.
A few more words on the ciphertext generated by SecKeyCreateEncryptedData
: since it’s generated by AES-GCM, besides the encypted data itself, it contains an authentication tag (a piece of information used to verify the integrity of the data). At the same time initialization vector (IV) is not put into the output because it’s generated together with the shared key. It means that both parties (Alice and Bob) can reconstruct not only the shared key used for encryption but also the IV used to initialize the AES-GCM procedure (see ECIES/ECDH scheme).
Here’s a short example demonstrating how to call the encryption and decryption functions. For encryption:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let clearText = "Hello" let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else { UIAlertController.showSimple(title: "Can't encrypt", text: "Algorith not supported", from: self) return } var error: Unmanaged<CFError>? let clearTextData = clearText.data(using: .utf8)! cipherTextData = SecKeyCreateEncryptedData(publicKey, algorithm, clearTextData as CFData, &error) as Data? guard cipherTextData != nil else { UIAlertController.showSimple(title: "Can't encrypt", text: (error!.takeRetainedValue() as Error).localizedDescription, from: self) return } // Use cipherTextData ... |
And for decryption:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// cipherTextData is our encrypted data let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM guard SecKeyIsAlgorithmSupported(self.key!, .decrypt, algorithm) else { UIAlertController.showSimple(title: "Can't decrypt", text: "Algorith not supported", from: self) return } // SecKeyCreateDecryptedData call is blocking when the used key // is protected by biometry authentication. If that's not the case, // dispatching to a background thread isn't necessary. DispatchQueue.global().async { var error: Unmanaged<CFError>? let clearTextData = SecKeyCreateDecryptedData(self.key!, algorithm, self.cipherTextData! as CFData, &error) as Data? DispatchQueue.main.async { guard clearTextData != nil else { UIAlertController.showSimple(title: "Can't decrypt", text: (error!.takeRetainedValue() as Error).localizedDescription, from: self) return } let clearText = String(decoding: clearTextData!, as: UTF8.self) // clearText is our decrypted string } } |
Note that we call SecKeyCreateDecryptedData
on a background thread because if our key is biometry-protected, then this call will trigger Touch ID or Face ID authentication UI and won’t return until user authenticates or cancels the authentication. We don’t want to block the main thread while this function waits for user authentication.
Signing and verifying signature
Keys that are stored in Secure Enclave can be used not only for data encryption but also for digital signature. To sign a message you need your private key (stored in Secure Enclave). To verify the signature another party needs the corresponding public key. Of course we suppose that the public key is handed to the other party in a secure manner to avoid man-in-the-middle attack.
For digital signature we will be using the following algorithm: ecdsaSignatureMessageX962SHA256
. This means that an ANSI X9.62-compliant version of ECDSA (Elliptic Curve Digital Signature Algorithm) is used and SHA256 is used as a cryptographic hash function. A SHA256 hash value is calculated for the original message and all the other steps of ECDSA are applied to it. By the way, if you already have that SHA256 value calculated, you can use the ecdsaSignatureDigestX962SHA256
algorithm which does exactly the same taking a SHA256 hash value as its input instead of the original message.
This is an example where we show how to calculate digital signature using our Secure Enclave-stored key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// cipherTextData is our encrypted data let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM guard SecKeyIsAlgorithmSupported(self.key!, .decrypt, algorithm) else { UIAlertController.showSimple(title: "Can't decrypt", text: "Algorith not supported", from: self) return } // SecKeyCreateDecryptedData call is blocking when the used key // is protected by biometry authentication. If that's not the case, // dispatching to a background thread isn't necessary. DispatchQueue.global().async { var error: Unmanaged<CFError>? let clearTextData = SecKeyCreateDecryptedData(self.key!, algorithm, self.cipherTextData! as CFData, &error) as Data? DispatchQueue.main.async { guard clearTextData != nil else { UIAlertController.showSimple(title: "Can't decrypt", text: (error!.takeRetainedValue() as Error).localizedDescription, from: self) return } let clearText = String(decoding: clearTextData!, as: UTF8.self) // clearText is our decrypted string } } |
First we check that our digital signature algorithm is supported and then the digital signature value is calculated by the SecKeyCreateSignature
function. We call it on a background thread to prevent the main thread from blocking in case our key is protected by biometry.
You can call the sign
function presented above in the following ways:
1 2 3 4 5 6 7 |
let clearTextData = clearText.data(using: .utf8)! // Signing the original message sign(algorithm: .ecdsaSignatureMessageX962SHA256, data: clearTextData) // Signing precalculated sha256 hash of the original message (produces the same result) sign(algorithm: .ecdsaSignatureDigestX962SHA256, data: sha256(data: clearTextData)) |
Note that these two calls will give you exactly the same result.
Now, let’s look how we can verify the signature that we just calculated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
guard let publicKey = SecKeyCopyPublicKey(key!) else { UIAlertController.showSimple(title: "Can't verify signature", text: "Can't get public key", from: self) return } let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256 guard SecKeyIsAlgorithmSupported(publicKey, .verify, algorithm) else { UIAlertController.showSimple(title: "Can't verify signature", text: "Algorithm is not supported", from: self) return } let clearTextData = clearText.data(using: .utf8)! var error: Unmanaged<CFError>? guard SecKeyVerifySignature(publicKey, algorithm, clearTextData as CFData, signature! as CFData, &error) else { // Can't verify/wrong signature // ... return } // Signature check: OK // ... |
Here we also check that the algorithm is supported and then call SecKeyVerifySignature
to verify the signatue. This function takes as its arguments the original message and the digital signature that we previously calculated. The function returns true
if the signature is valid.
I hope that this article will help you to make your iOS app more secure by applying some of the features coming with secure hardware element that’s included in iOS devices (Secure Enclave).
Full code samples for this article can be found here:
If you want to store small bits of generic data in iOS keychain (not EC encryption keys as described here) and protect them with a password or biometry (Touch ID/Face ID), you can take a look at my other posts: Password-protected entries in iOS keychain and Biometry-protected entries in iOS keychain.