Obvious vulnerabilities in self-proclaimed “most secure messenger”

- 14 min. - Linus Zvp

ginlo proclaims itself as the “most secure messenger”, for healthcare but also personal use, while lacking commonplace protections in the protocol and implementation of their mobile and web apps. Most issues were reported to ginlo on August 15th with a 90-day disclosure deadline, to which they said they were already aware of most of them. Besides swapping out an old PDF reader dependency, no fixes have been implemented at the time of writing.

Table of Contents

#1 Intro

There’s a German messenger app called ginlo that a few people seem to think (German) is a viable alternative to Signal. On its website, ginlo is advertised as follows:

Protect your messages
With the most secure messenger on the market. Made in Germany.

ginlo uses the strongest algorithms to protect your privacy and all your data. Not just while data is being transferred (end-to-end), but also when it’s on your device. That’s what full encryption means. Even ginlo as a provider doesn’t have any way of accessing your content.

Fully encrypted
Forget end-to-end encryption. The only way to ensure that your data is truly secure is with full encryption by ginlo.

I wanted to check the validity of those claims. The above advertising copy caused me to believe that they are trying to be a valid alternative to, e.g., Signal, so that’s the bar I set when evaluating the app. After reporting most of the issues and some further back and forth with the developers, they explained that they have a different target market than Signal and thus different goals for their cryptographic protocol. But why do they then keep the misleading ads on their page?

Ginlo’s marketing material says they additionally encrypt data at rest, but this is an industry practice for which they use the same library Signal uses. Further, ginlo provides no white paper that describes the algorithm. And although there are GitHub repositories that contain the source code of older versions of the Android and iOS apps, those barely contain any comments or documentation. Moreover, the provided Gradle config for Android Studio is broken, which disables all code-analysis features Android Studio usually has.

As such, I mostly base my findings on hours of reading the code by grep’ing through it. I focused mainly on how the app does cryptography and took a cursory look at the bundled dependencies. This is not exhaustive at all, as I did not have the time to investigate some leads.

As far as I understand, I can’t independently verify many of the issues by developing proof of concepts because German laws are stupid, and I don’t feel like getting sued. I will try to make the case for why these issues are valid by explaining the affected code. Later ginlo allowed me to debug their app, which is cool. But by that point I had done most of the source code review work already. They also did not directly confirm or reject the majority of the reported issues because I did not provide them with my real identity and did not want to sign an NDA.

Lastly, please excuse that certain parts of this post are a bit terse. I did not find the time to explain everything, but wanted to make people aware of this app’s shortcomings anyway.

#2 Identified issues

Ginlo’s terms of use state

  1. The algorithms and processes used for the encryption are regarded as secure pursuant to the latest technical standards (recommendations of the [German] Federal Office for Information Security).

Throughout this post, we will find multiple violations of the terms ginlo put themselves under.

#2.1 Outdated Protocol

For messages, ginlo uses Encrypt-Then-Sign. It encrypts messages with AES-256-CBC. Signing consists of two parts: It (1) computes the hashes of the message, sender, recipient, and any attachments individually and then (2) concatenates the hashes and signs the result with the 2048-bit RSA identity key. This is done twice, once with SHA-1 and then again with SHA-256. The currently selected hash is used for both (1) and (2).

This protocol has several downsides, apparent in the comparison of various secure messaging protocols by Unger et al. [1]. The one used by ginlo fits the “Static Asymmetric Cryptography” approach described in the paper.

Table 1: Comparing the security and privacy properties of Static Asymmetric Crypto and the 1-on-11 Signal Protocol (Authenticated DH+Double Ratchet+3DH AKE+Prekeys), redrawn and shortened from [1]. : provides property, : partially provides property, : does not provide property
Property Static Asymmetric Crypto Signal Protocol
Participant Consistency
Destination Validation
Forward Secrecy
Backward Secrecy
Anonymity Preserving
Speaker Consistency
Causality Preserving
Global Transcript
Message Unlinkability
Message Repudiation
Participation Repudiation

The paper lists the following issues that need to additionally be addressed when using static asymmetric crypto:

  • Forward & backward secrecy: missing
  • Destination validation: This is probably mitigated by signing something like \(h(\text{msg}) || h(\text{from}) || h(\text{to}) || h(\text{attm}_{1}) || \dots || h(\text{attm}_{n})\). See buildTextMessage (and surrounding methods for other MIME types), attachSignature() and getCombinedHashes()
  • Replay attacks: See sec. 2.1.2
  • Participant consistency: I did not look into this.

The identity keys of contacts can be verified by scanning their QR code.

Listing 1: Algorithm overview from GitHub.

public static final int AES_KEY_LENGTH = 256; public static final int IV_LENGTH = 128; public static final String DERIVE_ALGORITHM_SHA_256 = "PBKDF2WithHmacSHA256"; private static final int RSA_KEY_LENGTH = 2048; private static final int ROUNDS_ADMIN_CONSOLE = 8000; private static final String SIGNATURE_INSTANCE = "SHA1WithRSA"; private static final String SIGNATURE_INSTANCE_SHA256 = "SHA256WithRSA"; private static final String SIGNATURE_ALGORITHM = "SHA1WithRSAEncryption"; private static final String CN_LOCALHOST = "CN=localhost"; private static final String DERIVE_ALGORITHM = "PBKDF2WithHmacSHA1"; private static final String RANDOM_ALGORITHM = "SHA1PRNG"; private static final String RSA_GEN_ALGORITHM = "RSA"; private static final String RSA_CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA1AndMGF1Padding"; private static final String AES_GEN_ALGORITHM = "AES"; private static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; private static final String AES_CIPHER_ALGORITHM_NO_CBC = "AES"; private static final String AES_CIPHER_ALGORITHM_GCM = "AES/GCM/NoPadding";


The sensible thing to do would be to use the Signal protocol (see, e.g., this guide) or Messaging Layer Security (MLS). For anyone who doesn’t feel like reading the spec, the Security Cryptography Whatever podcast episode with one of the MLS spec’s co-authors is worth a listen. MLS also already has multiple implementations, some of which are open source.

#2.1.1 Algorithm selection

Severity: Informational
Confidence: Confirmed
Component(s): Identity keys
Status: Not fixed as of 2023-11-13

As shown in lst. 1, ginlo uses 2048-bit RSA keys. The BSI recommends phasing out 2048-bit RSA keys by the end of 2023. I have not found a mechanism for key rotation — but note that that does not necessarily mean that they don’t have code for it somewhere.

Further, NIST classifies RSA 2048 as operating at a security strength of 112 bits, while x25519 and x448, which Signal uses, provide 128 and 224 bits of security, respectively. Signal also just released PQXDH, a quantum resistant extension for the key-exchange, creating an even bigger gap between it and ginlo.

#2.1.2 Missing replay protection

Severity: Medium
Confidence: Confirmed
Component(s): 1-on-1 chats
Status: Not fixed as of 2023-11-13

Ginlo does not sign message identifiers and timestamps, or at least does not verify them on Android. Attackers in a position to perform a machine-in-the-middle (MITM) attack can abuse this to replay messages.

This does not only apply to nation states that can forge valid certificates: Many companies use TLS termination proxies to inspect employee traffic. Attackers that gain access to such a proxy, or ginlo’s servers, could perform this attack. This can also include disgruntled administrators. By shoulder-surfing or looking at people’s lock-screen they can find out the contents of messages they captured, even if they are not part of a conversation, and then replay them at opportune times.

Figure 1: Screen recording of a message replay attack. Two devices are used, device A and device B, with a Burp Suite TLS certificate installed on device B. The traffic of device B is proxied via Burp to simulate a MITM, such as ginlo’s servers or a corporate TLS proxy. First, Burp is instructed to intercept the reply to the getNewMessages command. Next, we see that the latest message device B received from device A was “message 6”. A then sends “message7”, which shows up in Burp as the response to the previous getNewMessages call to the server. This response is copied, to create a second message with the same contents, and its GUID and timestamp modified. Finally, the modified reply is forwarded to device B. Device B now shows that device A supposedly sent two messages and simply accepted the modified timestamp and GUID. If the above video does not work, try the mp4 version.
# Unknowns
  • Are groups and channels vulnerable to replay attacks?

#2.2 Lack of protocol versioning

Severity: Informational
Confidence: Confirmed
Component(s): Protocol versioning
Status: Not fixed as of 2023-11-13

Looking at lst. 2, it is evident that the protocol lacks a version field. This complicates migrations to new, improved versions of the protocol, as will be apparent in the next issue (sec. 2.2.1).

For a good description of why this is bad, see this post about issues in Threema.

Listing 2: Response to getNewMessages

HTTP/2 200 OK... Server: nginx Date: Fri, 22 Sep 2023 20:46:35 GMT Content-Type: application/json;charset=UTF-8 X-Content-Type-Options: nosniff X-Xss-Protection: 1; mode=block X-Dns-Prefetch-Control: off Vary: accept-encoding
[ { "PrivateMessage": { "senderId": "3000:{621f8a87-0753-48ed-b8f4-7a482c379e1d}",
"from": {... "0:{10a431c5-44f5-4fb8-b097-b383f941870e}": { "key": "DCGCmV+v5/XfC91rDrHGSdvYHmQ7rxxEkWd0GZc8r2zPflMCHlEmV1SNkcXYyPVvCcOKvcTGowPavtai3ueHDkKWGNFiNjGm+BOjM+E569ck+0elXhUSb2d1j6ugghmMhUikQPtIPjKI9ctKgVrVhS1FIj52FIbHNaY38jZKwR03yPa54t5PCziHATSVLUSgNUevGpPDmmIJHRIQ6LLTbgfRAyawvpyjnASLV8sqf8IJWLtFd7YzBu2G174uaecYJ9eS7JXBtn/yx4I6ArfHUbaRFNm+NZU2TWjeDXl1iGXxg1zlmaaj0hHpB6rJ7aksRFytBLdA5Oj+vRxjM/tMBA==", "key2": "Ke2xf9tpGgwRzFPTZR5dfRSN8m3zT3hD5WeVCLoOOpJUUVpdl9kqL8q6RDKKCgnN9iqBDejCdutcLi3W5Rq8n9JvQk262EjKzT9Zk4pLfJ0eX0J3Za5C2B3z+Nh6ng3itrmUNMDntdClA5DWV8bPcUyP2G2R+SlUHw16s7BHvWJrW6buWoqx3yguEef066O20aS/KlBD1eDsxNAb9EihV3sSvG6C1y5mYpz8+Gvd0uUsNsBEXIQqeFCFa3gbbgUAPxVG8rOUyRTfICDCfFR/UIZ+xqhc7XaN0/zH3DNlzJVL4geWezzEnQl3hCDV1GSLO8p4sMWuzUtrLwUM2LWeyA==", "nickname": "YQ==" } },
"to": [... { "0:{08118094-8cf4-4a7f-8930-719246162478}": { "key": "inPuRj3lXCWcZv8eNlsX4ZTDjshPrdrc727v8wi7XdBdbP2uWhF63u+uXfxM8kRmP6nDmMWUmYxSh7AOAMDyWqnUHMPW0DXdBtc0hGScwXPYpgYpsILcYSycbODh1NKAYwfak/uGZA/70xg1lNlJC4QrmSHusQeOtvcTPVN1Mh9V52Anx+NEsuMfbg4/j8CIR/CRoMTvYBEXPUTQ2yvRZo6qU0xsEJPEt66FnO50AIsV1/Jhobxy90KEbcLNj4hN7F2MerRVWPgixqrKGlBZAB8JUbhUoXTzQ+cAYfprjGOoUCTfN0XvR0uGPSDW4tdQpC3f6YNEAEVAO7sEEhDvAA==", "key2": "fuaEjuirseAYhNamq92M1Lqy9y+v8Xoep9Yascs+y600dyK91iyk7ldf83LQZG7MPQ5MwWYdnRnojG1ZvXvC2huAhGiwkPrinyJj0AxyV6dJmazB2PIfHhuQ6+jjKiWYA6KEYXn2IHTMRJsYeDNiKcJ9MK7/sr94WJ149FbaTBVtKm5zSvHYqvxAIEvskE1s/E+2QbTuX8HX47lZsRCYxDpJZr7cLxiKGBygeIz7ypYcpXbPcFyY85aGviK24vhnEyFEfLIOvNf4gwF83RJoZtbtEoU8YCtEZip45GV+bhuQYvAfliLyWUYplCQd86jNARwzEh+xWn747Z9a/IPKpA==" } } ],
"key2-iv": "OibMUew3loiE+I1R6LPjMA==", "data": "RfkafK3PXdgCi6Xu6nkYhoKT+ExymhvFWlmPlxeCZNCpQHzi8vIRQZVOj3n+ML0Hsgeww5+XUk5aK6mGqRhjcTp+VQZCaIJ+TS9IDZODbX2d+GLEAb+dyRGiydCU1QtxMlUlt3IgHiRL28ZbR0aB5+F4Q1pELyqCPg75KP79Sy7maMt9I73fGcpx37uuJIPf", "datesend": "2023-09-22 22:45:21.571+0200", "signature-sha256": {
"hashes": {... "from/0:{10a431c5-44f5-4fb8-b097-b383f941870e}": "a2f4969b7d621efaf55a3489cb9ebf1792a4bab35e8dbb8bd3d8510ee2772eb7", "from/0:{10a431c5-44f5-4fb8-b097-b383f941870e}/key": "5086c4b866a824d2127b08924af413d191b246e96834f8493dd65ae66371166e", "to/0:{08118094-8cf4-4a7f-8930-719246162478}": "ac007deda91477aec7653754f479b364d2e9f190d15d6e95ccc3bd178d991ff2", "to/0:{08118094-8cf4-4a7f-8930-719246162478}/key": "15cf44a44a59fbedf9744854c65d50afca6894b40c6478c0726f8d258f3f9edb", "data": "16cb11288ec3e480851ffa8ffc9351648ced5db08e7c2b8a6279934ca1a1dbba" },
"signature": "OOkjdRW/3L14iFNKVwUnGXLLCrdkVLtnffAnjekHkoMkDx9RR/wjcycFIcNcd/x8tyzXrrowPmhc\neiR64aVZrX15WHO23lH4+ECjyajLIItvBAalesBdvijQb3XtE91Ktfmw8YCAGaMqJafPVDAkARAE\nSoTI0qbg0xdTzGXqljfb0tzbr5iVRlb/oigcoSn3TfUsm7NjfK+/vNC0SusxMNjNclaWMLaVQR+p\nigH34/Gm5+2mTUYFuBiE16Op/2JYYHYl0+ajAkYemAXr8/B8UkaF9ZJjQYwCMsHkUvyW8nDZBl4c\nt70OKiwqMTb5jD9gVYam22MH+rbnJQBFHCW7ww==\n" }, "signature": {
"hashes": {... "from/0:{10a431c5-44f5-4fb8-b097-b383f941870e}": "7ac54dc66010124cecdae4936bc596e49f3b2d35", "from/0:{10a431c5-44f5-4fb8-b097-b383f941870e}/key": "aa5002c4542fd43a7949ab7a66bb7bb64be10010", "to/0:{08118094-8cf4-4a7f-8930-719246162478}": "8339a8ba3e2a9b9d9f57e3007295493d822c0475", "to/0:{08118094-8cf4-4a7f-8930-719246162478}/key": "f1b4d31c4aece92dab5d8e6e5c85ead5aae61576", "data": "1f52d4513b19407443ab80057228271a83ae4331" },
"signature": "cws7w3YO3e7/JQu4eHQfpHTyisHwg9/aFrDM+qhrC+5EoIEeed/8dXLeeQQb3hdlT+qa1IxAC8Rs\n1nZqzUyoq80SuhzWUmYmWBOKI0Su5UW60lhs2vDPyVqppwBRtVM3F6I8SojNkc+hnkHCb2ptPVb9\ne1v8QGjZcqro5SS65ruRnu4kUO/9XnC+o92uUt/LdZqhFzS7VFG9a2JvEMmwZsic6mOCdegovU8N\naNhBESkTCnmLkjXlencv5Z5ALZYAXYG/CSMm94TMQQG0Sf0Oq0k2Dt5hHekghUR0mHFdgKwxgybL\nrkIQUt56uDP19UQmoidmCEBw+mnnSljiGSnPBw==\n" }, "messageType": "text/plain", "attachment": [], "guid": "100:{621f8a87-0753-48ed-b8f4-7a482c379e1d}", "pushInfo": "push,sound" } } ]

I discovered this issue after reporting the second round of issues. Due to their previous replies, I did not report this to ginlo.

#2.2.1 Silent fallback to RSA+SHA-1

Severity: Medium
Confidence: Likely
Component(s): Message signature verification
Status: Not fixed as of 2023-11-13
CWE-327: Use of a Broken or Risky Cryptographic Algorithm
Comment: I used CVE-2022-29161, an issue in XWiki, for guidance. Use of RSA+SHA-1 for X509 certificates was rated medium by the XWiki devs and critical by NIST.

In lst. 2 there are both signature and signature-sha256 objects. If the signature-sha256 object is dropped, the app silently falls back to SHA-1, as shown in lst. 3.

Listing 3: Fallback to SHA-1 in getPreviewTextForMessage()

164boolean valid; 165if (decryptedMsg.getMessage() != null && decryptedMsg.getMessage().getSignatureSha256() != null) { 166 valid = checkSignatureSha256(decryptedMsg.getMessage()); 167} else { 168 valid = checkSignature(decryptedMsg.getMessage()); 169} 170 171if (!valid) { 172 throw new LocalizedException(LocalizedException.CHECK_SIGNATURE_FAILED, "Check Message signature failed."); 173}

This entirely defeats the purpose of the signatures over SHA-256 hashes. While finding a plausible-looking collision in text messages might be unlikely, finding one for documents has already been demonstrated.

I discovered this issue after reporting the second round of issues. Due to their previous replies, I did not report this to ginlo.

#2.3 Static AES key & IV pairs in groups

Severity: Medium
Confidence: Likely
Component(s): Group chats
Status: Not fixed as of 2023-11-13
CWE-329: Generation of Predictable IV with CBC Mode
Comment: The chosen plaintext attack mentioned in the CWE article does not apply in this instance, as there is no oracle an attacker who is not part of the group could abuse.

It seems like they set the AES secret key and IV at group creation, as seen in lst. 4, after which it is never updated again. The app sends new members the group AES key and IV as part of their invite (lst. 5). AES is used in CBC mode for encrypting group messages, so this is not a catastrophic failure. The information of which messages start with the same prefix is leaked, however.

As far as I can tell the app also does not rotate group keys after a member leaves or is removed from a group.

Listing 4: CreateGroupTask::doInBackground()

1781protected Void doInBackground(final Void... params) { 1782 try { 1783 final SecretKey aesKey = SecurityUtil.generateAESKey();
1784 final IvParameterSpec iv = SecurityUtil.generateIV();... 1785 final byte[] groupImageBytes; 1786 1787 if (mChatRoomImage != null) { 1788 final Bitmap bitmap = ImageUtil.decodeByteArray(mChatRoomImage); 1789 final Bitmap scaledBitmap = ImageUtil.getScaledImage(mApp.getResources(), bitmap, ImageUtil.SIZE_PROFILE_BIG); 1790 groupImageBytes = ImageUtil.compress(scaledBitmap, 50); 1791 } else { 1792 groupImageBytes = null; 1793 }
1794 1795 final ChatRoomModel chatRoomModel = buildChatRoom(mChatRoomName, mChatRoomType, groupImageBytes, aesKey, iv); 1796 final String groupInvMessagesAsJson = createGroupInvMessagesAsJson(chatRoomModel, aesKey, iv); 1797 // ...

Listing 5: Group invite creation (source).

1466String jsonInviteMsgs = null;... 1467if (mAddedMembers != null && mAddedMembers.size() > 0) { 1468 final ContactController contactController = mApp.getContactController(); 1469 final AccountController accountController = mApp.getAccountController(); 1470 final KeyController keyController = mApp.getKeyController(); 1471 final boolean sendProfileName = mApp.getPreferencesController().getSendProfileName(); 1472 final ArrayList<GroupInvMessageModel> groupInvites = new ArrayList<>(); 1473 final Account account = accountController.getAccount();
1474 final KeyPair keyPair = keyController.getUserKeyPair(); 1475 final SecretKey aesKey = mChat.getChatAESKey(); 1476 final IvParameterSpec iv = mChat.getChatInfoIV(); 1477 1478 List<Contact> loadedContacts = loadPublicKeys(null, mAddedMembers, contactController); 1479 1480 if (loadedContacts != null) { 1481 for (final Contact contact : loadedContacts) {
1482 try {... 1483 final String title = mChat.getTitle(); 1484
1485 final GroupInvMessageModel groupInviteMessageModel = createGroupInvMessage(contactController, 1486 mChat.getChatGuid(), title, mChat.getRoomType(), account, contact, keyPair, 1487 aesKey, iv, sendProfileName); 1488 1489 if (groupInviteMessageModel != null) { 1490 groupInvites.add(groupInviteMessageModel); 1491 }
1492 } catch (final LocalizedException e) {... 1493 LogUtil.w(TAG, e.getMessage(), e); 1494 mHasError = true; 1495 } 1496 } 1497
1498 jsonInviteMsgs = mGson.toJson(groupInvites.toArray(new GroupInvMessageModel[0]), GroupInvMessageModel[].class); 1499 } 1500}

This was discovered and reported after the initial round of vulnerabilities were reported. I did not provide a new deadline for this issue.

#2.3.1 Unknowns

  • Can one forge messages of another person?
  • Group membership might be handled by the server? Can the server add or refuse to kick people?

#2.4 Use of weak password hash

Severity: High
Confidence: Likely
Component(s): Backups, app password, admin console
Status: Not fixed as of 2023-11-13
CWE-916: Use of Password Hash With Insufficient Computational Effort

The app uses PBKDF2-SHA256 for hashing passwords. The number of rounds differs, but is always too low.

Since 2020-03-24 the BSI recommends argon2id for password hashing:

[…] If the use of a a cryptographic hardware token for password-based key derivation is not possible, the hash function Argon2id should be used. The security parameters of Argon2id and the requirements for the passwords depend on the application scenario and should be discussed with an expert.

So they are once again not following their own terms.

Regarding rounds, for the admin console password the app uses 8000 iterations. For the key storage one it’s 20000. OWASP recommends 600000 iterations. Before the app uses those keys, it XORs2 each of them with another key (XorKey) that is generated with Java’s SecureRandom. That doesn’t meet the goal of “full encryption” as defined by ginlo3. It doesn’t sufficiently protect the user if an attacker gains access to the keystore. The keystore is stored on the file system, which presumably includes the XorKeys. An attacker could then brute-force the weak keystore key.

8000 rounds in createAccountWithMdmData(). But I did not investigate what exactly it’s used for.

As mentioned above, the app uses 80000 rounds for backup passwords. For passwords using 100100 rounds of PBKDF2-SHA256 an RTX 4090 can try 14 million passwords in 3 minutes. ginlo does not store a hash of the password (which is the secret key used to encrypt the backups) in their backups, so it is not as simple as just brute-forcing the hash: the resulting digest needs to be used with AES in CBC mode to decrypt one of the json blobs.

There is a hashcat implementation of a similar algorithm for VMware VMX files, which use PBKDF2-HMAC-SHA1 + AES-256-CBC (ginlo uses AES-128-CBC). The PBKDF2 rounds used in this module’s benchmark are 10000 instead of ginlo’s 80000 for backups. With the default configuration, an RTX 4090 can do approx. 1MH/s (search for 27400). I don’t feel like spending even more time on this to adjust the above algorithm for ginlo’s data format. I hope it’s obvious that the number of rounds is not enough. \(\frac{1\text{ MH/s}}{80000 / 10000}\approx 125\text{kH/s},\) meaning 125000 tries per second per 4090, is still pretty bad.

What makes the low rounds for backups worse is that backups to iCloud/Google Drive seem to be allowed by default, according to the documentation (see point 29).

So users who store their backups in the cloud and who use a weak backup password (or still have a backup with an old and weak password somewhere) could see their account compromised. Since static asymmetric keys are used, which do not provide forward or backward secrecy, the compromise of the static RSA key is enough to decrypt any of a user’s future or past messages, as well as to impersonate them.

The admin console key is likely used for the management cockpit ginlo provides for administrators:

ginlo’s Management Cockpit increases security

Central user management, stronger protection against malware

If the servers are compromised or the operators turn malicious, the keys would be relatively easy4 to brute-force.


Switch to argon2id and follow the procedure for parameter selection outlined its RFC.

If PBKDF2 can’t be replaced, switch from SHA-1 to SHA-512 and also follow the procedure outlined in the above RFC. However, be aware that this does not obviate the need for strong passwords.

#2.4.1 Unknowns

  • They hardcode a KEYSTORE_PASS at build time. Is that only used if the user does not set a password?

#2.4.2 Brute-forcing backup passwords

Ginlo backups are .zip files that contain various json files, two of them are shown below.

Listing 6: Contents of the info.json file. Only the pbkdfSalt field changes between backups. salt seems to be a parameter used to potentially denylist old backups, but is not used for that as of writing.

[ { "BackupInfo": { "version": "1", "app": "ginlo", "salt": "$2a$04$Dsvymn7LlP1bMlTCuNpd/O", "pbdkfSalt": "gDeLao6snCY=", "pbdkfRounds": "80000" } } ]

Listing 7: Contents of the decrypted accounts.json file. Data is partially truncated.

[ { "AccountBackup": { "guid": "0:{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}", "nickname": "a", "phone": "", "profileKey": "ll25R...", "publicKey": "\\u003cRSAKeyValue...", "accountID": "J5RGG2W3", "privateKey": "\\u003cRSAKeyValue...", "mandant": "default", "backupPasstoken": "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" } } ]

Small oddity: When the user restores a backup, the app checks whether the backup’s salt is in the list of allowed salts. The list of allowed salts is set by the BuildConfig.

Here’s a small PoC for brute-forcing backup passwords:

Listing 8: Python proof of concept for brute-forcing ginlo backups. It’s single-threaded python code and thus only does approximately 10000 guesses in 6 minutes. Porting to hashcat would significantly increase the hash rate.

1#!/usr/bin/env python3 2 3# slow proof of concept for brute-forcing ginlo backup passwords 4# $ 7z x ginlo-backup-$ID.zip 5# $ pip install pycryptodome 6# $ ./decrypt.py password_list 7 8import hashlib 9import json 10import base64 11import sys 12from Cryptodome.Cipher import AES 13 14with open("info.json", "r") as infojson: 15 info = json.load(infojson)[0]["BackupInfo"] 16 salt = info["salt"] 17 pbkdf_salt = base64.b64decode(info["pbdkfSalt"]) 18 pbkdf_rounds = info["pbdkfRounds"] 19 20with open("account.json", "rb") as accountjson: 21 account_enc = accountjson.read() 22 23iv = account_enc[:16] 24 25with open(sys.argv[1], "r") as pws: 26 for password in pws: 27 digest = hashlib.pbkdf2_hmac("sha1", password.encode(), pbkdf_salt, 80000, 32) 28 cipher = AES.new(digest, AES.MODE_CBC, iv) 29 account_dec = cipher.decrypt(account_enc[16:]) 30 if account_dec[:20] == b'[{"AccountBackup":{"': 31 account_json = json.loads(account_dec)[0]["AccountBackup"] 32 print(f"password: {password}") 33 print( 34 f"guid: {account_json['guid']}\n" 35 f"nickname: {account_json['nickname']}\n" 36 f"accountID: {account_json['accountID']}\n" 37 f"privateKey: {account_json['privateKey']}" 38 ) 39 break


Enforce the BSI, NIST or the new PCI guidelines. To compare against passwords from past breaches, you can use the Pwned Passwords API from HaveIBeenPwned.

#2.5 Bundled PDFium is severely outdated

Severity: High
Confidence: Confirmed
Component(s): AndroidPdfViewer
Status: Fixed in version, released on 2023-08-21
CWE-1104: Use of Unmaintained Third Party Components

The .gitmodules file shows that ginlo uses forks of jitsi-meet and AndroidPdfViewer.

The forked AndroidPdfViewer in turn uses a forked PdfiumAndroid. All the forked projects don’t have any additional commits, they’re old snapshots.

PdfiumAndroid uses the pdfium library from the Android Open Source Project, the one from Chromium. The last commit in the forked repository was in 2018, to update the library to the one used in Android 7.1.2.

I think it’s safe to assume that the PDF library hasn’t been updated in over 5 years. In the meantime, a number of vulnerabilities were identified in it5. And while there is a setting to switch to use an external app to view PDFs, the user is warned that the file is no longer protected by ginlo in that case. The built-in one is used by default.

PDFRenderer, the OS-provided PDF renderer introduced as part of Android 5.0 (SDK version 21) in 2014, should be used Instead of the bundled, ancient version of pdfium. This is also the very same minimum Android version that the app supports.

#2.6 Web client code authenticity issues

There appears to be no way for users of the web app to verify that the code they are running has not been tampered with.

An attacker that compromises the server could serve malicious JavaScript to users and compromise their keys and chat history. See also the comment of a Signal dev about this (mirror). Even Facebook provides an extension that can check the authenticity of the WhatsApp, Facebook, Messenger and Instagram websites.

I discovered this issue after reporting the second round of issues. Due to their previous replies, I did not report this to ginlo.

#3 Conclusion

For some, ginlo may have privacy upsides compared to its alternatives. Regarding security, while it does not appear to be completely broken, ginlo is lacking compared to Signal and many of its alternatives. It turned out that the app is based on the code of the SIMSme app (German) the German Federal Post Office developed in 2014 and later sold off. The protocol they are using is roughly the same the SIMSme app used almost a decade ago.

Also, using modern cryptography and modern protocols is not the only thing that is important for keeping users secure. You also need to keep up with security updates of your dependencies, or find alternatives if they are unmaintained and unlikely to be entirely free from security issues. Especially so if they parse potentially untrusted input, like PDFs from strangers.

Even Facebook Messenger uses the Signal protocol now [2]. So, technically, using Facebook’s Messenger is more secure (not private) than using ginlo.

#4 Disclosure Timeline

I omit their justifications/explanations because I didn’t ask whether I can share them publicly.

  • 2023-08-15: Report sent to ginlo with a 90-day deadline.
  • 2023-08-16: Reply from the team at ginlo, saying they are aware of most of the issues. They ask me to share my identity before sharing more details.
  • 2023-08-20: I reply, declining the request to share my identity and urge them to at the very least do some of the simpler mitigations, like increasing PBKDF2 rounds. I also recommended to have any fixes reviewed by competent security consultants and tell them to find and look at public reports to find a good company.
  • 2023-08-21: version removes pdfium, which was still present in version (see libjniPdfium.so & libmodpdfium.so)
  • 2023-09-03: I reply with my suspicion about the use of static AES keys & IVs for groups and ask for permission to use frida to confirm the issues I found.
  • 2023-09-07: The contact at ginlo apologised for the delay, as they were on holidays
  • 2023-09-11: The contact at ginlo replied to my mail from 2023-09-03 and asks for confirmation that I’m not working for a competitor, in which case they would give me permission to debug their app. They once again offer more details in return for sharing my identity. They note that they removed the outdated PDF library.
  • 2023-09-17: I reply saying that I’m not working for one of ginlo’s competitors. For work, I’m a security consultant and am in this case doing this on my own personal time.
  • 2023-09-21: They reply, saying that they don’t mean to advertise ginlo as the most secure messenger on the market. They once again offer more details in return for me signing an NDA. They also said they’d be on holiday for 2 weeks.
  • 2023-10-17: I reply, citing their advertising copy where they advertise ginlo as the most secure messenger. I add that I do not intend to do my job for free and offer to refer them to my superiors at work if they really want to work with me. They have not replied since.
  • 2023-11-13: Release of this blog post.


N. Unger et al., “SoK: Secure Messaging,” in 2015 IEEE Symposium on Security and Privacy, San Jose, CA, USA: IEEE, May 2015, pp. 232–249. doi: 10.1109/SP.2015.22.
Cited: 1, 2, 3
T. Isobe and R. Ito, “Security Analysis of End-to-End Encryption for Zoom Meetings,” IEEE Access, vol. 9, pp. 90677–90689, 2021, doi: 10.1109/ACCESS.2021.3091722.
Cited: 1