End-to-end encryption

End-to-end encryption (E2EE) is a secure communication method that prevents third parties and intermediaries, including the service provider itself, from accessing data while it is transferred between an app and a server. End-to-end encryption is based on public key infrastructure (PKI) and uses asymmetric encryption to protect data as it travels back and forth between the app and the server.

  1. Both the app and the server generate a pair of keys: one public and one private. The public key is used to encrypt data, while the private key is used to decrypt it.

  2. The app and server exchange public keys with each other. Public keys can be shared openly without compromising the security of their corresponding private keys.

  3. Before sending data to the server the app encrypts it using the server’s public key. Since only the server holds its unique private key, only it can decrypt this data.

  4. Similarly, when the server sends data back to the app, it uses the app's public key to encrypt the data, ensuring that only the app can decrypt it using its private key.

The exchange of public keys is crucial for secure end-to-end encrypted communication, as it enables each endpoint to encrypt messages in such a way that only the intended recipient with the matching private key is capable of decrypting and thus understanding them.

While the above is true, a man-in-the-middle attack can be employed to substitute public keys and decrypt intercepted messages for further analysis, whether pursuing reverse engineering or other purposes. Recipients, however, would not be able to decrypt messages encrypted with substituted keys.

Digest:

End-to-end encryption is asymmetric, meaning it uses two different but mathematically linked keys to encrypt and decrypt data.

A public key can only be used to encrypt data, which makes it safe to share with others without worrying about unintended recipients decrypting it.

In end-to-end encryption, the public key exchange phase is amongst the most critical ones for guarantying the transferred data safety.

It’s possible to use man-in-the-middle attack to substitute public keys during the handshake enabling its decryption.

SSL pinning can mitigate such risks by guaranteeing that keys are only exchanged with the known server.

SSL pinning

SSL pinning is a security process that ensures an app can only communicate with a specific server whose SSL certificate details are stored in the app. This adds an extra layer of security to protect against man-in-the-middle (MITM) attacks, which occur when the real server's public key is replaced during a handshake process. Such manipulation of the public key infrastructure (PKI) allows for secure and encrypted communication to be intercepted and decrypted.

SSL pinning includes two common techniques:

  1. Certificate pinning: The app stores the complete certificate of a known server. During the SSL handshake, if the server's certificate does not match the stored one, the connection is terminated.

  2. Public key pinning: The app stores only the public key from the certificate, rather than the whole certificate. To establish trust, the app verifies that the public key embedded in the server's certificate matches the locally stored one.

The difference between the two approaches comes down to flexibility. Public key pinning offers more flexibility by allowing you to replace certificates as long as they are signed with the same private key. In contrast, certificate pinning requires updating the app with a new certificate every time the server's certificate changes.

SSL pinning can be easily implemented in Swift using either the popular TrustKit framework or by configuring Alamofire, which you might already be using.

import Alamofire
import Foundation
import Security
// 1. Obtain the public key for the cat-fact.herokuapp.com server using OpenSSL:
// ```
// openssl s_client -servername cat-fact.herokuapp.com -connect cat-fact.herokuapp.com:443 | openssl x509 -pubkey -noout
// ```
let publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqg+OEpiG/p3j7WkF+oF370r9SaZpWhwyOV7vdhXFhapCZYbnaOuJL7UWT4l27AoRq2O6+6k9zLa6M8atwFs/VGFT3L+GEC7y5+W0q8D/Q031fmvKIIsBP/NFeNd4vVM/l6EfcgEtrghwqgPl8gmt5DQhWYFHQY85owIaoHFqy3eFAhLcORoFedW/MmjpQ27QR7T3h1/hxgPDEp+wxfTCmn1OCUT7g6RUd8Ya08jDPF/g6Vf1Qmu4+GrHa2xdWxOS7+158TFv1A9wcMDb1HC16zHvQA+T8a+ebpErXiBWmUNzvhYknNJXLfJWiT///9eXm3c6zzdmrWUoKuGegd+1kQIDAQAB"
guard let data = Data(base64Encoded: publicKey) else {
  fatalError("Can't decode public key data: \(publicKey)")
}
// 2. Create SecKey from the public key.
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic]
guard let key = SecKeyCreateWithData(data as CFData, attributes as CFDictionary, nil) else {
  fatalError("Can't create SecKey with the public key: \(publicKey)")
}
// 3. Create associated `host: publicKey` evaluator dictionary.
let evaluators = [
  "cat-fact.herokuapp.com": PublicKeysTrustEvaluator(keys: [key]),
]
// 4. Set up Alamofire trust manager and session.
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
let session = Session(serverTrustManager: serverTrustManager)
// 5. Fetch and print cat facts!
print("😺 Loading cat facts with SSL pinning…")
let result = try await withCheckedThrowingContinuation({ continuation in
  session.request(URL(string: "https://cat-fact.herokuapp.com/facts")!).response { response in
    do {
      let json = try JSONSerialization.jsonObject(with: response.data!)
        continuation.resume(returning: json)
      } catch {
        continuation.resume(throwing: error)
      }
    }
  }
})
print(result)

Digest:

Changing the private key used for certificate signing will invalidate the pinned public key, which may cause the app to stop functioning.

Pinning a certificate is a more fragile approach, as the app will need to be updated every time there is a certificate change on the server.

Updating the pinned certificate or public key can be extremely challenging, if not impossible, if it occurs in an older version of the app that is no longer actively maintained.

While SSL pinning is very effective, it can be a risky feature without a fallback strategy. Make sure to weigh all the pros and cons before implementing it.