End-to-End Encrypted iOS Chat with Apple’s CryptoKit

In most cases, when building a chat app, it's essential to provide adequate privacy and security to your users. This can be done using cryptographic methods such as end-to-end encryption. End-to-end encryption is becoming a mainstream expectation, as it's featured in the biggest chat apps, such as WhatsApp and Telegram.

In this article, you'll learn how basic end-to-end encryption works in an iOS chat app using Apple's own CryptoKit framework for its secure and high-level encryption methods and Stream's flexible iOS chat SDK for its ready-made chat networking and UI components.

Please note that this tutorial is very basic and strictly educational, may contain simplifications, and rolling your own encryption protocol is not advisable. The algorithms used can contain certain 'gotchas' if not employed properly with the help of security professionals. Still, reading this article is a great start to get practical knowledge.

You can find the completed project on GitHub: encrypted-ios-chat. And if you have any questions, feel free to reach out to me on Twitter: @cardosodev :).

What Is End-to-End Encryption?

End-to-end encryption is a communication system where the only people who can read the messages are the people communicating. No eavesdropper can access the cryptographic keys needed to decrypt the conversation—not even a company that runs the messaging service.

What Is Apple's CryptoKit?

CryptoKit is a new Swift framework that makes it easier and safer than ever to perform cryptographic operations, whether you simply need to compute a hash or are implementing a more advanced authentication protocol.

What You Need

  • Xcode 11 or later
  • iOS 13 or later
  • A Stream account

Basic Encryption Methods

Before we get to the chat part, let's start by defining the basic encryption methods we need. We'll integrate these methods in the next section.

1. Generating a Private Key

A private key is essential to end-to-end encryption. It is used alongside a public key to generate a symmetric key which is used for encryption and decryption of data. Each user in your application has a private and public key. They share their public keys with each other so that they can generate the same symmetric key. Alice shares her public key with Bob, and Bob uses his private key and Alice's public key to generate a symmetric key. Conversely, Bob shares his public key with Alice, and Alice uses her private key with Bob's public key to generate the same symmetric key. As long as Alice's and Bob's private keys are a secret to each of them, no one can generate the same symmetric key to decrypt their conversations.

To generate a private key, we'll use the P256.KeyAgreement.PrivateKey() initializer.

Additionally, I chose the P256 algorithm for its cross-platform availability, as it is supported by the Web Crypto API in case you need a web version. Also, it's a good balance between performance and security. This preference can change with time as new algorithms become available.

PS: The public key can be extracted from the private key using privateKey.publicKey. We'll touch on this in the integration part of this article.

The private key must be saved somewhere safe. In order to do this, you may need to turn it into a string format to save it and back to perform the cryptographic operations. You can do this with the following exportPrivateKey method:

And the importPrivateKey method:

2. Deriving Symmetric Key

Once a user (message sender) has the public key of the other user it wants to communicate with (message recipient), that user can generate the symmetric key using the methods: privateKey.sharedSecretFromKeyAgreement(with: publicKey) and sharedSecret.hkdfDerivedSymmetricKey.

This process is called a Diffie-Hellman Key Exchange and ensures the symmetric key can be shared between two users without ever leaving their devices.

3. Encrypt Text

Now, we use that symmetric key for encrypting texts using the AES.GCM.seal method. We also encode it in a Base64 string, so it can be sent as normal text using the Stream Chat SDK in the integration step.

After the text is encrypted and encoded, it can be safely sent through the network.

4. Decrypt Text

After the recipient receives the encrypted text, they will decode it from Base64 and decrypt it using the AES.GCM.SealedBox and AES.GCM.open methods.

After the text is decrypted, it should be readable and can be displayed in the UI.

Stream Chat Integration

In this section, you'll learn how to use the methods implemented above to achieve end-to-end encryption with Stream Chat's Swift SDK.

You can follow the "iOS Chat SDK Setup" step in the official tutorial to get a basic chat app working in minutes.

1. Public Key Extra Data

We need to define a custom publicKey field for the User object so that users can share their public key with others. To do this, create the PublicKeyExtraData type.

After that, you should set the User.extraDataType to PublicKeyExtraData.self before you initialize the Stream Chat Client in AppDelegate.swift:

2. Custom Set User Method

Now, let's define our custom setUser method so that the user can be registered in Stream Chat alongside its public key.

Additionally, the public key must be exported in a string format. You should define an exportPublicKey method to do this.

We're percent-encoding the public key after encoding to base64 to ensure the + sign is not ignored in case the public key is used in the URLs of specific requests.

In a future step, after we fetch another user's public key, we'll need to use the importPublicKey method, which does the reverse of the exportPublicKey.

3. Encrypted Chat View Controller

Now that we generated the private key and registered our user with a public key, we need a custom ChatViewController that encrypts messages before sending and decrypts messages before displaying.

To do this, let's create the EncryptedChatViewController, which inherits from ChatViewController.

As you can see, it contains an additional symmetricKey property, which will be used for the encryption and decryption. In the viewDidLoad we set the messagePreparationCallback, which encrypts the messages before they're sent. We also override the messageCell method to decrypt the message before displaying it.

3. Present Encrypted Chat

After implementing our custom ChatViewController, now we can present it.

To present the EncryptedChatViewController, we query the user we want to communicate with to get its public key and generate a symmetric key. After that, we set the symmetricKey and presenter properties and push the view controller into the navigation stack.

Next Steps With CryptoKit

Congratulations! You just learned how to implement basic end-to-end encryption in your iOS chat apps. It's important to know this is the most basic form of end-to-end encryption. It lacks some additional tweaks that can make it more bullet-proof for the real world, such as randomized padding, digital signature, and forward secrecy, among others. Also, for real-world usage, it's vital to get the help of application security professionals.