Pitfalls of rolling your own E2EE protocol
Or: An example of a great vendor response
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”1, Javier Anton talked about their app “Collaborative Groups”, which the website2 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
recommends3 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 bits4.
Usage of a non-cryptographically secure RNG
The ChatKey
is generated a bit differently:

// 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 ChatKey
s are generated using java.util.Random()
, which in
the Codename One library the app’s author uses is described as5:
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 entirely6 based on current system
time but its use for security-sensitive applications is discouraged nonetheless
and SecureRandom
is recommended instead7 (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
4.
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 implemented4.
No message authentication
It seems that no MAC8 is used to prove integrity of chat messages, which can enable e.g. padding oracle attacks9, due to CBC’s partial malleability10. To make it short: please use an algorithm that provides authentication (see AEAD11), like AES-GCM-SIV. And then make use of the AAD to prevent replay attacks12.
Javier mentioned13 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-Poly13054, 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 message14.
The app’s author said that ChatKey
s 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.

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 frida15 or MITMing the connection of one’s own device16 to obtain the token.
A rate limit of 50 email searches per day has since been implemented4.
Comment on risk assessment
In the blog post4 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:
- MITM the connection between client and server (this is what he focuses on)
- 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
ChatKey
s. 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 future4.
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.
- Symmetric encryption key generation was time-based
- A non-cryptographically secure key generator was used
- RSA key length was only considered secure until end of 2022 and protects everything
- 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
- The symmetric
ChatKey
s are changed once a month by default, resulting in mediocre forward secrecy - Encrypted data wasn’t authenticated and thus potentially vulnerable to a padding oracle attack
- Replay attacks still possible
- Though from the conversation with the author I’m somewhat optimistic that it’ll be fixed in the future
- 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 libsignal19.
While some guidance was provided to Javier, the updated app has not been reviewed or tested. This should not be interpreted as an audit or the like.
-
https://www.groupsapp.online/post/improved-e2e-encryption ↩︎
-
https://codenameone.com/javadoc/index.html?java/util/Random.html ↩︎
-
https://android.googlesource.com/platform/dalvik/+/f87ab9616697b8bae08c5e8007cbdd0039a1f8ce/libcore/luni/src/main/java/java/util/Random.java#60 ↩︎
-
https://de.wikipedia.org/wiki/Keyed-Hash_Message_Authentication_Code ↩︎
-
https://en.wikipedia.org/wiki/Malleability_(cryptography) ↩︎
-
https://signal.org/docs/specifications/doubleratchet/#symmetric-key-ratchet ↩︎
-
See Burp Suite, Zed Attack Proxy and others ↩︎
-
https://developer.android.com/training/articles/security-config#CertificatePinning ↩︎
-
https://square.github.io/okhttp/4.x/okhttp/okhttp3/-certificate-pinner/ ↩︎