Cross Platform ECIES encryption with Swift & Apple's Secure Enclave

A discussion of fundamentals, and some examples of cross-platform Elliptic curve-based hybrid cryptography compatible with Apple platforms and hardware-bound keys.

Apple's Secure Enclave; amongst it's many other utilities, provides excellent opportunities for data integrity and confidentiality. The ability to encipher data only decryptable by a single device and hardware-bound key is a powerful workflow; potentially even more so when this data originates from cross-platform infrastructure or applications.

For an upcoming project, I needed a consistent way to exchange encrypted data between a series of microservices and Apple devices and keys held in their Secure Enclaves. This short article will briefly touch on some hybrid cryptography fundamentals and one of Apple's implementations as part of their Security framework as well as links to some new resources I have created to help build and understand cross-platform applications.

Whilst this article and the projects and code samples it links to are inherently technical and require a base understanding of programming in Swift and Golang, the underlying concepts may be valuable to non-programmers with an interest in security on Apple platforms. In particular, I've tried to step out my commentary in the ECIES-Swift-Playground described below to be relevant to someone not familiar with the actual code and help to establish an understanding of the fundamentals.

Whilst CryptoKit provides a modern hybrid cryptography option through its implementation of the HKPE standard, this can prove a more complex build out due to the myriad of AEADs, KDFs and KEMs supported by the standard, it's nonce and sequence generation and use of authenticated encryption. Elliptic Curve Integrated Encryption Scheme (ECIES) is the primary Elliptic Curve encryption standard supported by Apple's Security framework and is a great choice for simple, secure data exchange. It shares a lot of connective tissue with HKPE, but is a little easier to implement across platforms (I plan to write a follow-up article discussing a similar implementation of cross-platform HPKE senders and receivers, and will go into more detail about this).

ECIES is a hybrid encryption scheme using Elliptic Curve keys supported by the Secure Enclave (when using a NIST P-256 curve). As a hybrid scheme, it uses asymmetric keys exchanged and trusted between a sender and receiver to derive a symmetric key that is used to actually encrypt the data. It is simple to encrypt and decrypt data on Apple platforms using this scheme, as Apple provides functions that handle most of the lift for you; namely ephemeral key generation, ECDH exchange, symmetric key derivation, nonce generation, AES-GCM sealing/opening as well as encapsulating the ciphertext along with the asymmetric key. David Schuetz provides an excellent breakdown of Apple's ECIES implementation here, and has done a huge amount of detective work to document the technical underpinnings of the process.

When data is encrypted on an Apple device under the scheme, this is effectively the process that happens behind the scenes:

Selection of the underlying hashing algorithm and variable IV setting used during the process is governed by a SecKeyAlgorithm; a Swift type that determines which hash the KDF function uses (SHA-224, SHA-256, SHA-384, ect.) and how the Initialisation Vector (IV) for the symmetric AES-GCM encryption is computed.

When exchanging data between Apple platforms and devices, these resultant "Combined Ciphertext Data" objects can be decrypted back to plaintext with the appropriate key simply by passing them (along with the correct algorithm selection) to SecKeyCreateDecryptedData(). The receiver retrieves and uses the ephemeral public key to perform the ECDH with their private pairing and derives the same shared key that was used to encrypt. This is great if you are working entirely within an Apple ecosystem, but if you want to exchange data cross-platform or with a backend service, good examples have been harder to find.

If you search around online for ECIES examples, you are likely to come across many that utilise the EC curve secp256k1, most known for its use as part of Bitcoin and other cryptocurrencies. Note that this curve is different from secp256r1 (which is supported by Apple implementations whereas secp256k1 is not), leading to many ECIES implementations (that hardcode the use of k1) being incompatible with Apple's. To aid in a better understanding on how to exchange cross-platform encrypted data with Apple platforms, I've created the following open source resources:

  • ECIES - a Go module that implements functions to perform ECIES encryption, decryption and key derivation compatible with Security framework. It basically does the same uplift as Apple performs for you in their Swift/Objective-C API.
  • ECIES-Go-Example - a sample Go project that utilises the ECIES module to encrypt and decrypt some example data.
  • ECIES-Swift-Playground - a macOS Swift playground that details the process of importing public and private keys from OpenSSL PEM representations, then encrypting and decrypting. It can be used alongside ECIES-Go-Example to learn how to exchange ECIES encrypted data cross-platform.

These projects and samples taken together, whilst hardly revolutionary, should contribute a fair bit towards learning and coding cross-platform ECIES data exchange. With all this in mind, encrypting data for a device's Secure Enclave is fairly simple. We can take Apple's documentation around the protection of keys with the Secure Enclave here:

Protecting keys with the Secure Enclave | Apple Developer Documentation
Create an extra layer of security for your private keys.

And use their examples to build out something like this:

do {
  // create our access control policy
  // so that our key is only accessible when the device is unlocked
  // and valid biometrics are presented
  var accessError: Unmanaged<CFError>?
  guard let access = SecAccessControlCreateWithFlags(
      [.privateKeyUsage, .biometryAny],
      &accessError) else {
          throw accessError!.takeRetainedValue() as Error

  // setup our key creation attributes
  // for a 256 bit (P-256) EC key generated by the Secure Enclave
  // and with a tag we can use to query and use it later
  let attributes: NSDictionary = [
              kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
              kSecAttrKeySizeInBits: 256,
              kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
              kSecPrivateKeyAttrs: [
                  kSecAttrIsPermanent: true,
                  kSecAttrApplicationTag: "me.jedda.ECIESSecureEnclaveExample.key",
                  kSecAttrAccessControl: access

  // create our key
  var createError: Unmanaged<CFError>?
  guard let secKey = SecKeyCreateRandomKey(attributes, &createError) else {
    throw createError!.takeRetainedValue() as Error

  let publicSecKey = SecKeyCopyPublicKey(secKey)
  var publicSecKeyError: Unmanaged<CFError>?
  guard let publicKeyData = SecKeyCopyExternalRepresentation(publicSecKey!, &publicSecKeyError) as? Data else {
            throw createError!.takeRetainedValue() as Error

  // now we have our raw public key bytes in publicKeyData
  // which should be easily portable to wherever they need to go
  // we can encrypt data with this key, and it will only be decryptable
  // on this device with the hardware bound key in the Secure Enclave

} catch {
  print("ERROR \(error)")

You need to think carefully about how you use this type of encryption. Remember that with non-extractable keys; if the device dies or is lost, the data is effectively crypto-shredded. For short-lived messages and commands, this might be desirable, but just make sure that you consider these risks if you choose to encrypt anything more permanent.

If you use some of the resources I've created and linked to above, I hope you find them useful. If you have any questions or feedback, please reach out.

© 2024 Jedda Wignall