Categories
Security Swift

Store data, encrypted

Today I want to share a helper class I worked on a while back. The requirement was that some (large) piece of data needed to be stored on the device, it should be stored encrypted. File protection complete was deemed not enough.

The idea was that a piece of data needed to be stored on disk, with a key stored in the keychain, in the secure enclave if available.

Just a few lines, but there’s actually a lot going on. The code created was inspired by various bits and pieces online and it took a bit of trial and error to get right.

First things first. We need to determine if a Secure Enclave is available and functional.

import Foundation
import os.log

enum CryptoError: Error {
    case keyCreationFailed
}
class Crypto {
    
    static var shared: Crypto {
        return self.sharedInstance
    }
    
    private static let sharedInstance = Crypto()
    
    private let keyName = "disk_storage_key"
    
    private let hasSecurityEnclave: Bool
    private let algorithm: SecKeyAlgorithm
    private let keySize: Int
    private let keyType: CFString
    
    init() {
        var attributes = [String: Any]()
        attributes[kSecAttrKeyType as String] = kSecAttrKeyTypeEC
        attributes[kSecAttrKeySizeInBits as String] = 256
        attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
        
        var error: Unmanaged?
        let randomKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
        
        if randomKey != nil {
            self.hasSecurityEnclave = true
            self.keySize = 256
            self.keyType = kSecAttrKeyTypeEC
            self.algorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
        } else {
            self.hasSecurityEnclave = false
            self.keySize = 4096
            self.keyType = kSecAttrKeyTypeRSA
            self.algorithm = .rsaEncryptionOAEPSHA512AESGCM
        }
    }
}

That actually quite a lot of code already isn’t it. But if you follow along in the above code, it’s not that complicated. We just try to create a random key that requires the Secure Enclave. To my knowledge this generates a key pair, but does not store anything in the keychain or the Secure Enclave. Based on the result, a couple properties are stored on the Crypto class.

Generating a new key pair

Next up is generating a key pair and actually hanging on to it.

private func makeAndStoreKey(name: String) throws -> SecKey {
        
        guard let access =
            SecAccessControlCreateWithFlags(
                kCFAllocatorDefault,
                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                self.hasSecurityEnclave ? SecAccessControlCreateFlags.privateKeyUsage : [],
                nil) else {
                    throw CryptoError.keyCreationFailed
        }
        var attributes = [String: Any]()
        attributes[kSecAttrKeyType as String] = self.keyType
        attributes[kSecAttrKeySizeInBits as String] = self.keySize
        #if targetEnvironment(simulator)
        print("SIMULATOR does not support secure enclave.")
        #else
        if self.hasSecurityEnclave {
            attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
        }
        #endif

        let tag = name
        attributes[kSecPrivateKeyAttrs as String] = [
            kSecAttrIsPermanent as String: true,
            kSecAttrApplicationTag as String: tag,
            kSecAttrAccessControl as String: access
        ]

        var error: Unmanaged?
        let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)

        if let error = error {
            throw error.takeRetainedValue() as Error
        }

        guard let unwrappedPrivateKey = privateKey else {
            throw CryptoError.keyCreationFailed
        }

        return unwrappedPrivateKey
    }

Why is using these keychain functions so verbose? A lot of stuff going on. But if you read through it carefully you will see that, again, a key pair is generated, but now we actually hang on to it. See that kSecAttrIsPermanent being give a value of true?

Also notice that there is some code to deal with a simulator not having a Secure Enclave.

Loading a key

We did most of the hard work already now. We also need some code to load a key. We’ll get to choosing between loading and generating in a bit. Key take away in the next bit is that the resulting SecKey is optional. We just might try and load a key and if it fails, we create one…

    private func loadKey(name: String) -> SecKey? {
        let tag = name
        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: tag,
            kSecAttrKeyType as String: self.keyType,
            kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
            kSecReturnRef as String: true
        ]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        guard status == errSecSuccess else {
            return nil
        }
        return (item as! SecKey) // swiftlint:disable:this force_cast
    }

Let’s encrypt

Now we are getting to the good part. Let’s encrypt (pun intended).

func encrypt(data clearTextData: Data) throws -> Data? {
        let key = try loadKey(name: keyName) ?? makeAndStoreKey(name: keyName)

        guard let publicKey = SecKeyCopyPublicKey(key) else {
            // Can't get public key
            return nil
        }
        let algorithm: SecKeyAlgorithm = self.algorithm
        guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else {
            os_log("Can't encrypt. Algorithm not supported.", log: Log.crypto, type: .error)
            return nil
        }
        var error: Unmanaged?
        let cipherTextData = SecKeyCreateEncryptedData(publicKey, algorithm,
                                                       clearTextData as CFData,
                                                       &error) as Data?
        if let error = error {
            os_log("Can't encrypt. %{public}@", log: Log.crypto, type: .error, (error.takeRetainedValue() as Error).localizedDescription)
            return nil
        }
        guard cipherTextData != nil else {
            os_log("Can't encrypt. No resulting cipherTextData", log: Log.crypto, type: .error)
            return nil
        }

        os_log("Encrypted data.", log: Log.crypto, type: .info)
        return cipherTextData
    }

Did you notice the optional chaining on the loadKey call?

Let’s decrypt

Having encrypted data is fun and all, but we do need some way to decrypt it again. Notice that in the next bit, being unable to load a key is a failure. When decrypting we do expect that a key is already present.

func decrypt(data cipherTextData: Data) -> Data? {
        guard let key = loadKey(name: keyName) else { return nil }

        let algorithm: SecKeyAlgorithm = self.algorithm
        guard SecKeyIsAlgorithmSupported(key, .decrypt, algorithm) else {
            os_log("Can't decrypt. Algorithm not supported.", log: Log.crypto, type: .error)
            return nil
        }

        var error: Unmanaged?
        let clearTextData = SecKeyCreateDecryptedData(key,
                                                      algorithm,
                                                      cipherTextData as CFData,
                                                      &error) as Data?
        if let error = error {
            os_log("Can't decrypt. %{public}@", log: Log.crypto, type: .error, (error.takeRetainedValue() as Error).localizedDescription)
            return nil
        }
        guard clearTextData != nil else {
            os_log("Can't decrypt. No resulting cleartextData.", log: Log.crypto, type: .error)
            return nil
        }
        os_log("Decrypted data.", log: Log.crypto, type: .info)
        return clearTextData
    }

Conclusions

A couple things to notice in all of the above.

By Jeroen

I’m Jeroen a dutch software developer employed by a large insurance company. Mostly I write about Swift code, CocoaHeadsNL and other related topics.

Leave a Reply

Your email address will not be published. Required fields are marked *