How VivoPay Leveraged the Secure Enclave and CryptoKit

Ronald Mannak
Coinmonks
8 min readSep 23, 2020

--

Apple’s T2 Secure Enclave chip in an Intel Mac (the Secure Enclave in iOS devices and Apple Silicon Macs is integrated in Apple’s Axx SoC)

VivoPay is an insanely secure crypto wallet and makes use of the Secure Enclave found in modern mobile devices and Macs and Apple’s new CryptoKit framework.

Secure Enclave

The Secure Enclave is a chip in Android, iPhone, iPad and Macs to secure your biometrical data like FaceID and TouchID. The Secure Enclave is also accessible to developers and can handle a small number of useful cryptographic functions:

  • Generate private keys (and derive the public key) and store the private keys generated on the Secure Enclave (you can’t store private keys generated outside of the Secure Enclave)
  • Encrypting data (plaintext) by sending plaintext to the Secure Enclave. The Secure Enclave will send back the encrypted data (ciphertext)
  • Decrypting data by sending ciphertext to the Secure Enclave. The Secure Enclave will send back the original plain text.
  • Create a signature by sending (the hash) of data to the Secure Enclave. The Secure Enclave will send back a signature
  • Verify a signature by sending (the hash) of data plus a signature to the Secure Enclave. The Secure Enclave will return true or false.

So is it as simple as having the Secure Enclave create a Harmony One private key? Well, not quite. It turns out the Secure Enclave chips (on both Android and Apple) use a different curve than blockchains use. That makes it impossible to create keypairs for blockchain on the Secure Enclave.

Besides, since a private key cannot be exported from the Secure Enclave, there is no way to back up the private key. If the device gets lost or damaged, the private key is lost for ever. So even if it was possible (it isn’t) it’s probably still not a good idea.

As a side-note: It is possible to build a blockchain from scratch using the Secure Enclave if you really want to. I created a blockchain for educational purposes back in 2018 that did just that.

In VivoPay we use the Secure Enclave to encrypt and decrypt the wallet file, so that a wallet file can only be used on the device it was created. This improves security. We use Apple’s low level Security framework for Secure Enclave computation.

We also use a high level framework called CryptoKit to encrypt the backup file (if the user uses the default backup and not the recovery phrase).

Apple CryptoKit

There was some confusion when Apple launched CryptoKit in 2019. Some people (and publications!) thought it meant Apple embraced cryptocurrencies and CryptoKit was a cryptocurrency framework. Unfortunately, that wasn’t the case as I explained in a tweetstorm. Instead, CryptoKit provides a high level interface to a few basic features of the Secure Enclave, like creating private keys (that are not compatible with blockchains) and basic encryption and signing targeted towards establishing SSL communication.

In VivoPay, we use CryptoKit for encrypting the wallet backup file in the default mode (the advanced mode forces users to write down a recovery phrase). In the VivoPay Encryption source code, the relevant file is Shared/Encryption/BackupEncryption.swift. The encryption and decryption is stored in a simple struct called BackupEncryption, and there’s a convenience init for the CryptoKit class SymmetricKey in an extension at the bottom of the file.

The BackupEncryption file contains three methods:

  • Create a symmetric key based on a password chosen by the user
  • Encrypt the wallet using the symmetric key
  • Decrypt the wallet using the symmetric key

We use a 256 bit key for the symmetric encryption and create one by calling a SymmetricKey convenience initializer. Because the password could be shorter than 256 bits (=32 bytes), we take the SHA256 hash of the password and use the first 32 bytes of the hash as the key.

init(password: String) {    let hash = SHA256.hash(data: password.data(using: .utf8)!)    // Convert the SHA256 to a string. This will be a 64 byte string    let hashString = hash.map { String(format: "%02hhx", $0) }.joined()    // Convert to 32 bytes / 256 bits    let subString = String(hashString.prefix(32))    // Convert the substring to data    let keyData = subString.data(using: .utf8)!    self.init(data: keyData)}

Once the key is created, it can be used for symmetric encryption and decryption (in a symmetric cipher). CryptoKit provides two symmetric ciphers: Advanced Encryption Standard (AES) cipher and the ChaCha20-Poly1305 cipher. ChaChaPoly is optimized for mobile use, and we chose to ChaChaPoly in VivoPay for that reason.

With the SymmetricKey convenience init, encrypting is trivial:

func encrypt(_ clearText: String, with password: String) throws -> Data {    let key = SymmetricKey(password: password)    let data = clearText.data(using: .utf8)!    return try ChaChaPoly.seal(data, using: key).combined}

So is decrypting. ChaChaPoly stores the encrypted data (ciphertext) in a ‘sealed box’ container. The container contains the ciphertext itself plus a nonce. The nonce is an automatically and randomly generated number to avoid duplicates in a situation data is exchanged between parties (it is not relevant to our use case). The nonce also makes it possible to encrypt and decrypt data portions in parallel, which is good news, but outside the scope of this blog post.

func decrypt(_ cipherText: Data, with password: String) throws -> String? {    let key = SymmetricKey(password: password)    let sealedBox = try ChaChaPoly.SealedBox(combined: cipherText)    let decryptedData = try ChaChaPoly.open(sealedBox, using: key)    return String(data: decryptedData, encoding: .utf8)}

And that’s it. That’s all the code needed to create a password encrypted backup.

Since the symmetric key can be created using a password anywhere and isn’t stored in the Secure Enclave, the encrypted data can be encrypted by anyone with access to the password on any device. That’s exactly what we need for a backup, but that’s not what we want for the wallet file itself.

Apple’s Security Framework

In contrast to other wallets, copying wallets files between different devices doesn’t work. An encrypted wallet file is always tied to the device the file was created on. If you want to use the same wallet on multiple devices, you have to recover a wallet using the recovery phrase or the backup file on iCloud. This way, VivoPay is more secure than other software wallets.

To make this work, the wallet file has to be encrypted by either a symmetric key created and stored in the secure enclave or a public key that belongs to a private key created and stored in the secure enclave. Unfortunately, CryptoKit does not provide an API for that and we’ll have to fall back to the low level Security framework.

The steps are similar to CryptoKit, but the code is more elaborate. You will find the relevant code in the VivoPay Encryption source code: the relevant file is Shared/Encryption/WalletEncryption.swift.

You might notice an import CryptoKit statement in the file. That is because we’re using one shortcut from CryptoKit: SecureEnclave.isAvailable

The code contains a fall back to Keychain in case the device doesn’t have a Secure Enclave (such as the iPod Touch or Macs without a T1 or T2 chip)

The code then sets all the needed attributes (not shown in the code below but available in the demo project) to create a private key.

The private key is generated with the function SecKeyCreateRandomKey and the public key is the derived from the private with by calling SecKeyCopyPublicKey. Note that the public key object contains the public key, but the private key object returned by SecKeyCreateRandomKey does not contain the private key. Instead, the object contains a reference to the private key that remains in the Secure Enclave.

fileprivate func createKeys() throws  -> (public: SecKey, private: SecKey?) {    var error: Unmanaged<CFError>?    let privateKeyAccessControl: SecAccessControlCreateFlags = SecureEnclave.isAvailable ?  [.userPresence, .privateKeyUsage] : [.userPresence]    guard let privateKeyAccess = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, privateKeyAccessControl, &error) else {
throw error!.takeRetainedValue() as Error
}
var privateKeyAttributes: [String: Any] = [
...
]
var commonKeyAttributes: [String: Any] = [
...
]
// Create new private key using attributes defined above
guard
let privateKey = SecKeyCreateRandomKey(commonKeyAttributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
// Derive public key
guard
let publicKey = SecKeyCopyPublicKey(privateKey) else {
throw VivoError.encryption("Error creating public key")
}
return (public: publicKey, private: privateKey)}

The reference to the private key and the public key can be retrieved and restored with the method loadKey. LoadKey creates a query and sends the query to the Secure Enclave using the function SecItemCopyMatching.

fileprivate func loadKey() throws -> SecKey? {    // Create query
var query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "vivopaydemo",
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecReturnRef as String: true
]
// Use secure enclave if possible
if SecureEnclave.isAvailable {
query[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
}
// Obtain reference to private key
var key: CFTypeRef?
if SecItemCopyMatching(query as CFDictionary, &key) == errSecSuccess {
return (key as! SecKey)
}
// No existing key was found
return nil
}

Encrypting is as follows:

func encrypt(_ data: Data) throws -> Data {    // Verify public key can be used to encrypt
guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, WalletEncryption.encryptionAlgorithm) else {
throw VivoError.encryption("Error verifying public key")
}
// Encrypt
var error: Unmanaged<CFError>?
guard let cipherText = SecKeyCreateEncryptedData(publicKey, WalletEncryption.encryptionAlgorithm, data as CFData, &error) as Data? else {
throw error!.takeRetainedValue() as Error
}
return cipherText
}

And decrypting is pretty straightforward as well:

func decrypt(_ cipherText: Data) throws -> Data {    // Verify private key can be used to decrypt
guard let privateKey = privateKey, SecKeyIsAlgorithmSupported(privateKey, .decrypt, WalletEncryption.encryptionAlgorithm) else {
throw VivoError.encryption("Error fetching private key")
}
// Decrypt data
var error: Unmanaged<CFError>?
guard let clearText = SecKeyCreateDecryptedData(privateKey, WalletEncryption.encryptionAlgorithm, cipherText as CFData, &error) as Data? else {
throw error!.takeRetainedValue() as Error
}
// Return clear text
return data
}

The code contains an unused deleteKey method, which is useful to experiment and debug the demo code.

Where to go next?

Also, Read

--

--