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<CFError>?
        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<CFError>?
        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<CFError>?
        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<CFError>?
        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.

Categories
Security Swift

Client certificate with URLSession in Swift

Recently we I needed to implement an API in an app requiring a TLS client certificate.

It proved to be pretty simple, but I did need connect various bits and pieces together to get to a working solution. In this post I’ll show what worked for me.

To learn more about client certificate: https://en.wikipedia.org/wiki/TransportLayerSecurity#Client-authenticatedTLShandshake

Basically it involves a few things.

  • A client certificate, in my case packaged as a p12 file.
  • A password for the p12 files.
  • A cocoa pod called ASN1Decoder, to allow interpretation of ASN.1 formatted data.
  • An URLSessionDelegate
  • Once successfully obtaining the client certificate identity, we can create an URLCredential to respond to the challenge.

So here is the lowdown extracted from the session delegate I created:

public class MyURLSessionDelegate: NSObject, URLSessionDelegate {
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        // `NSURLAuthenticationMethodClientCertificate`
        // indicates the server requested a client certificate.
        if challenge.protectionSpace.authenticationMethod
             != NSURLAuthenticationMethodClientCertificate {
                completionHandler(.performDefaultHandling, nil)
                return
        }

        guard let file = Bundle(for: HTTPAccessURLSessionDelegate.self).url(forResource: p12Filename, withExtension: "p12"),
              let p12Data = try? Data(contentsOf: file) else {
            // Loading of the p12 file's data failed.
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // Interpret the data in the P12 data blob with
        // a little helper class called `PKCS12`.
        let password = "MyP12Password" // Obviously this should be stored or entered more securely.
        let p12Contents = PKCS12(pkcs12Data: p12Data, password: password)
        guard let identity = p12Contents.identity else {
            // Creating a PKCS12 never fails, but interpretting th contained data can. So again, no identity? We fall back to default.
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // In my case, and as Apple recommends,
        // we do not pass the certificate chain into
        // the URLCredential used to respond to the challenge.
        let credential = URLCredential(identity: identity,
                                   certificates: nil,
                                    persistence: .none)
        challenge.sender?.use(credential, for: challenge)
        completionHandler(.useCredential, credential)
    }
}

As you can see, there is a lot of “if it fails we go to default” going on. This is security related code, so if things don’t work, we do not try any recovery, default handling just implies that no client certificate will be used and thus the connect should fail.

Here is the PKCS12 implementation. It is actually based on https://gist.github.com/algal/66703927b8379182640a42294e5f3c0b It is basically some helper code to bridge Core Foundation types into the memory safety of Swift.

private class PKCS12 {
    let label: String?
    let keyID: NSData?
    let trust: SecTrust?
    let certChain: [SecTrust]?
    let identity: SecIdentity?

    /// Creates a PKCS12 instance from a piece of data.
    /// - Parameters:
    ///   - pkcs12Data:
              the actual data we want to parse.
    ///   - password:
              The password required to unlock the PKCS12 data.
    public init(pkcs12Data: Data, password: String) {
        let importPasswordOption: NSDictionary
          = [kSecImportExportPassphrase as NSString: password]
        var items: CFArray?
        let secError: OSStatus
          = SecPKCS12Import(pkcs12Data as NSData,
                            importPasswordOption, &items)
        guard secError == errSecSuccess else {
            if secError == errSecAuthFailed {
                NSLog("Incorrect password?")

            }
            fatalError("Error trying to import PKCS12 data")
        }
        guard let theItemsCFArray = items else { fatalError() }
        let theItemsNSArray: NSArray = theItemsCFArray as NSArray
        guard let dictArray
          = theItemsNSArray as? [[String: AnyObject]] else {
            fatalError()
          }
        func f<T>(key: CFString) -> T? {
            for dict in dictArray {
                if let value = dict[key as String] as? T {
                    return value
                }
              }
            return nil
        }
        self.label = f(key: kSecImportItemLabel)
        self.keyID = f(key: kSecImportItemKeyID)
        self.trust = f(key: kSecImportItemTrust)
        self.certChain = f(key: kSecImportItemCertChain)
        self.identity = f(key: kSecImportItemIdentity)

    }
}

Any question? I’ll gladly answer any questions you might have.