Encrypted notifications on iOS

David Frenkel
4 min readAug 27, 2018

You might not know, but notifications from different services all pass through Apple’s servers before they reach your phone. They start at the servers of the service (Facebook, Twitter or Tink), pass through Apple and land on your iPhone.

For the most part, someone following you on Twitter, getting a comment on Facebook or someone liking your photo on Instagram isn’t of big consequence. On the other hand details about your personal finances are very significant.

Hopefully, Apple isn’t interested in your notifications and they are already secured with SSL during transport but there are a number of malicious actors out there who would be. Especially when some banking apps offer to notify you every time you spend and include the name and/or location of the purchase, it provides a goldmine of information on the user. So what do you do? You encrypt the notifications.

The Basics

We wanted to send arbitrary sized messages as secure as possible to the client. For this, we picked RSA + AES encryption. When the user logs in, the client generates the RSA key pair and sends the public key to the backend.

When the backend sends a notification it will generate an AES key and use that to encrypt the notification message then, encrypts the AES key with the client generated RSA public key.

iOS generates RSA keys without the “header” describing their specification. This makes it incompatible with other systems. We used this to add back the header manually. More on this later.

The notification arrives. On iOS, you need to use a NotificationService extension if you want to modify the notification before it’s displayed. Because it happens while you might be playing Candy Crush or watching a Youtube video NotificationServices have a set memory and time limit to do their job. These limits were increased for Swift based apps but it’s still good to keep in mind, you need to be quick.

The Solution

The notification arrives and the service starts up. It’s time to decrypt the payload and mutate the notification to display the decrypted message.

Step one is to get our private RSA key. It was generated in the main target in our app. It’s stored in the secure enclave and can be queried by using the same tag and setting the same type.

Important, in the kSecPrivateKeyAttrs make sure to set kSecAttrAccessible as kSecAttrAccessibleAfterFirstUnlock. Why? The secure enclave is locked when the device is locked. It’s also the time most of your notifications arrive. Setting kSecAttrAccessibleAfterFirstUnlock will keep that RSA key unlocked indefinitely once the user unlocks it.

We convert the sent encrypted AES key into Data so we can decrypt it using SecKeyCreateDecryptedData and convert it into a String.

Side note, there is a built-in way to encrypt and decrypt RSA+AES in one swoop. The issue (as noted above) with that is iOS does not include the header that describes the encryption type used. This makes it non-standard and incompatible with frameworks used on the backend and other OSs.

So far easy, everything we’ve done has been built into iOS. We have the decrypted AES key and the encrypted payload. First, you need the IV (initialization vector), it’s the first 16 bytes of the encrypted payload String turned into Data. Once you have the IV you turn it, the AES key and payload into Data and use this amazing StackOverflow answer (scroll down for Swift 3/4 implementation with Data input types) that took me 4 days to find, to decrypt the encrypted payload. Not included in the answer, remove the first 16 bytes before returning from the function. It’s the IV.

The End

That is it, you’re done. It took us about a week, sending a couple hundred test notifications and an 8pm Slack remote desktop/video session to figure this out (you can only use remote desktop if you download Slack from their website and not from the App Store).

It’s not much code but it’s hard to debug. Breakpoints can’t help because there is a time limit, so you need to use a lot of print statements. Also, print statements don’t work because it’s a separate service so you need to use os_log.

There are alternatives. There are a number of frameworks you can use. Generally if they are easy to use they tend to have one problem. They re-implement the (open source) encryption solutions which give them an easy to use API but it means they can’t use the built-in hardware encryption/decryption powers of the device. They rely solely on software which makes them a couple hundred times slower than using the more complicated native APIs.

--

--