Pitfalls of rolling your own E2EE protocol

Or: An example of a great vendor response

Posted on

This post includes all the contents of the previous one on telegra.ph. Everything from No message authentication onwards is new or updated.


Intro

In a recent HN thread titled “My small revenge on Apple”[^3], Javier Anton talked about their app “Collaborative Groups”, which the website[^4] claims is end-to-end encrypted. There is no source available, so all the below is based on a short reverse engineering session of the Android app. This is not meant as an attack on the author. Instead, this should highlight some of the pitfalls of rolling your own encryption protocol.

The part of the protocol seen in the of this short session works like this: when two parties want to chat, the app generates a random encryption key and encrypts that with the RSA key of the other party. This way, the server can’t decrypt the chat key and thus can’t read your messages (in theory).

Identified issues

The RSA key generation happens on the device, which is good, and uses SecureRandom which is also good. The key size is 2048 bits, which the BSI recommends[^5] to use only until end of 2022.

// com.groups.network.m20.i0
// compiled from ChatUtils

// in function: k
RSAKeyPairGenerator k2Var = new RSAKeyPairGenerator();
k2Var.mo10576c(new RSAKeyGenerationParameters(new BigInteger("10001", 16), new SecureRandom(), 2048, 80));

The app’s author says the RSA key size has been updated to 3072 bits[^2].

Usage of a non-cryptographically secure RNG

The ChatKey is generated a bit differently:

`ChatKey` references, among them a call to `generateRand16ByteString()`, used for initialisation
// com.groups.network.m20.kh
// compiled from: Utils

// function renamed from: V
public String generateRand16ByteString() {
    Random random = new Random();
    StringBuffer stringBuffer = new StringBuffer();
    while (stringBuffer.length() < 16) {
        stringBuffer.append(Integer.toHexString(random.nextInt()));
    }
    return stringBuffer.toString().substring(0, 16).toUpperCase();
}

It looks like the ChatKeys are generated using java.util.Random(), which in the Codename One library the app’s author uses is described as[^6]:

public Random()

Creates a new random number generator. Its seed is initialized to a value based on the current time: public Random() { this(System.currentTimeMillis()); }

On Android, the standard Random is not entirely[^7] based on current system time but its use for security-sensitive applications is discouraged nonetheless and SecureRandom is recommended instead[^8] (Note: it’s unknown whether iOS has the same issues). However, it does not seem like Codename One uses that. The documentation says it’s purely time-based.

Even if it were random, though, successive outputs of a non-CSPRNG typically reveal the internal state of the RNG. That’s why one just doesn’t use this for key generation. In this case, the RNG is reinitialized for every key generation run. Depending on the RNG it uses (again, non-CSPRNG does not make guarantees here), assuming a few starting bytes might allow you to determine what later outputs have to be, reducing the key’s strength significantly because there are only so many possible starting sequences. If the RNG’s seed wasn’t weak already.

The app’s author says Random has been replaced with SecureRandom[^2].

No key verification

Additionally, it does not appear possible to verify keys. A chat was started with a random stranger and no method was found to verify either the RSA key or the ChatKey. This means that you have to trust the server to give you the right RSA key; the server could just give you a different key and intercept the connection and the average user could never tell.

This was confirmed by the app’s author and has since been implemented[^2].

No message authentication

It seems that no MAC[^11] is used to prove integrity of chat messages, which can enable e.g. padding oracle attacks[^12], due to CBC’s partial malleability[^16]. To make it short: please use an algorithm that provides authentication (see AEAD[^13]), like AES-GCM-SIV. And then make use of the AAD to prevent replay attacks[^10].

Javier mentioned[^1] that he noticed some e2ee error messages after my initial post. While I had nothing to do with that, my best guess would be that someone was trying out padding oracle attacks.

I noticed some E2E error messages coming from the trace which means that you (I assume it was you) have been tinkering with it/trying to break it.

The app’s encryption algorithm has since been switched to XChaCha20-Poly1305[^2], which is also an AEAD algorithm. Though replay attacks are not yet mitigated.

Mediocre forward secrecy

For forward secrecy the device key usually stays the same, while the key messages are encrypted with will change. In the Signal protocol they change for each message[^18].

The app’s author said that ChatKeys are rotated once a month by default and a shorter duration can be manually set.

All users' names, surnames and emails exposed

As seen below, it’s apparently possible to search the whole user directory for either

  • first name,
  • last name or
  • email.
Contact search showing many results

This wasn’t rate limited. The app’s author says the actual email search is done in the backend via a “startswith” search. Additionally, clients are required to authenticate via a token. But even so, verified email addresses could easily be harvested by e.g. instrumenting the app with frida[^19] or MITMing the connection of one’s own device[^20] to obtain the token.

A rate limit of 50 email searches per day has since been implemented[^2].

Comment on risk assessment

In the blog post[^2] Javier Anton mostly focuses on “active” MITM attacks and only mentions the PRNG issues as an aside and that only after he updated the blog post. That is, in my opinion, the wrong way around.

There are two ways to pull off the active MITM attack Javier describes:

  1. MITM the connection between client and server (this is what he focuses on)
    • this might include having to get a TLS certificate from somewhere
    • can be prevented by employing certificate pinning[^14][^15].
  2. hack into the app’s servers (not mentioned at all)
    • then there’s no need to get any TLS certificates

“Active” MITM

Once an attacker pulled off the above attack, they could MITM the initial RSA key TOFU process or re-initiate it for both parties and from then on capture, modify and possibly forge new messages.

“Passive” MITM

But that’s not the only way if the ChatKey is predictable and rarely, if ever, changes. Since once an attacker gets onto the app’s servers they can just decrypt every message (and modify and re-encrypt it) using the time-based ChatKeys. If the keys had been generated using a CSPRNG, that would not be possible and only in that case the “active” MITM would be the only way to capture (and/or modify) the message contents.

By allowing users to verify RSA keys and by using a CSPRNG for key generation it won’t be possible to decrypt or even re-encrypt messages in the future, even by an active MITM. But only if the participants verified fingerprints out-of-band and if the app warns when RSA keys change.

Certificate pinning won’t be implemented in the foreseeable future[^2].

Recommendations

✅ Implemented
🟧 Partially implemented
❌ Not implemented

  • ✅ Use SecureRandom for cryptographic key generation
  • ✅ Let participants verify RSA key fingerprints out of bounds
  • ✅ Use AEAD, like AES-GCM-SIV
  • ❌ Prevent replay attacks by making use of AEAD’s AAD
  • ✅ Fix RSA key length
  • ❌ Implement certificate pinning
  • 🟧 Do not reveal the names of all users or allow enumeration of email addresses
  • 🟧 Use short-lived session keys for proper forward secrecy

TL;DR

The app’s author fixed almost all of the issues within a week, which is quite exceptional.

  1. Symmetric encryption key generation was time-based
  2. A non-cryptographically secure key generator was used
  3. RSA key length was only considered secure until end of 2022 and protects everything
  4. You trusted the server because you couldn’t verify the encryption keys in-person (or at least out-of-band, like via PGP or Signal)
    • This kind of defeats the point of e2ee
  5. The symmetric ChatKeys are changed once a month by default, resulting in mediocre forward secrecy
  6. Encrypted data wasn’t authenticated and thus potentially vulnerable to a padding oracle attack
  7. Replay attacks still possible
    • Though from the conversation with the author I’m somewhat optimistic that it’ll be fixed in the future
  8. Apparently the whole user directory is visible to every user

If you’re going to advertise e2ee in something other people use, then it should be sound. If it hadn’t been advertised that would’ve been fine. If it had been marked as experimental that would’ve been fine, too. But as it stood it was not OK.

I hope others can learn from both the app author’s mistakes and his exceptional response.

For anyone wanting e2ee in their own app, it might be a good idea to use libsignal[^21].