Freedom Chat's End-to-End Encryption (E2EE)

In this article we provide a detailed analysis of Freedom Chat’s end-to-end encryption as well as snippets of our actual code base. If you would like to read our white paper you can do so here. 1/15/24

End-to-End Encryption in Freedom Chat

End-to-end encryption (E2EE) is crucial for modern messaging apps and is designed to prevent anyone other than the intended recipient of a message from reading it. When E2EE is not implemented (either knowingly or improperly), not only can the provider of the messaging app you’re using be able to read your messages but malicious hackers and even government agencies can too. In fact, research estimates that over 5.5 billion unencrypted texts are sent each day!

We created Freedom Chat to give everyone control over their digital lives where you can communicate with whomever you want (privately), protect your data, avoid having your conversations “leaked” or read by nosey tyrants.

End-to-End Encryption Meaning

End-to-end encryption works by leveraging asymmetric cryptography, a kind of encryption first invented by the GCHQ in 1973 but was kept classified until 1997. The general public version of this kind of encryption was published a few years later in 1977 by Rivest, Shamir and Adleman who founded the company bearing their name, RSA. Asymmetric cryptography is at the core of modern communication: almost all internet or phone communications are protected using asymmetric cryptography (including the internet connection you’re using to read this article).

The way such algorithms work is based on each participant generating a “private key”, which is kept private. From this private key, the participant derives the corresponding “public key”, and shares it publicly. Using someone’s public key, a user can encrypt a message that only the bearer of the private key will be able to decrypt. Often, a public key is compared to an open vault, and the private key is compared to the code of that vault: anyone can close the vault, but only the person who knows the code can open it back.

Freedom Chat end-to-end encryption

In essence, when a user wants to encrypt a message for another user, they simply use the recipient’s public key to encrypt it. The encrypted message is then sent via Freedom Chat servers, and the recipient uses their private key to decrypt it.

Freedom Chat end-to-end encryption

In Freedom Chat, RSA keys with a modulus size of 4096 bits for asymmetric cryptography are generated upon account creation, then stored encrypted with a symmetric key safely stored in the device’s keychain.

Trust On First-Use

An attack scenario described in the previous paragraph lies in how the public key is sent from the recipient to the sender. If an attacker manages to sneak in and swap the recipient’s public key with an attacker-controlled public key, the sender would receive the attacker’s public key and use it to encrypt the secret message not for the recipient, but for the attacker.

Freedom Chat end-to-end encryption

This attack is known as a “man-in-the-middle attack”. To prevent a man-in-the-middle attack, there needs to be a mechanism to authenticate the recipient’s public key, so that we can be sure we use a legitimate public key later in the process.

In Freedom Chat, we have multiple layers to ensure this authentication:

1. Freedom Chat locally stores the `sealdId` of any user it interacted with, so that if anyone tried to swap the keys with attacker-controlled public keys after they have been stored for the first time, it would be detected.

2. Freedom Chat stores a signature chain for each user in which the various public keys of a given user are mutually signed along with the `sealdId`, so that if anyone tried to maliciously update the keyring of a user, the newly added public keys would be detected and rejected. To do so, here is the helper function used to detect such discrepancies:

				
					
const getSealdIds = async(chatId, phoneNumbers) => {
    let discrepancies = 0;
    const query = db.collection("sealdIdentities").where(firebase.firestore.FieldPath.documentId(), "in", phoneNumbers);
    const snapshot = await query.get();
    const localMembers = await getChatMembers(chatId);
    if (localMembers) {
        const localMap = new Map();
        localMembers.map(member => localMap.set(member.phone_number, member.seald_id));
        snapshot.docs.map(doc => {
            const localSealdId = localMap.get(doc.id);
            if (localSealdId !== doc.data().sealdId) {
                discrepancies++;
            }
        });
    }
    return { discrepancies, data: snapshot.docs.map(doc => ({phoneNumber: doc.id, sealdId: doc.data().sealdId})) }
}
				
			

This is called the “Trust On First-Use” (TOFU) paradigm: as soon as your Freedom Chat app has interacted with another user, it keeps enough information locally about that user to detect a potential future attack. This makes attacks much more difficult in practice, as it makes them easily detectable. As to detecting an attacker that tries to swap the recipient’s keys before first use, we will cover this below.

Encrypting A Chat

In order to encrypt a chat, a Freedom Chat user generates randomly what is called a “symmetric key” and encrypts it with the public keys of the recipients. The encrypted public keys transit via a server to each recipient, and each of them can use their own private key to decrypt it and recover the symmetric key.

				
					
// sender
const session = await seald.createEncryptionSession({ sealdIds: [ /* list of recipients' sealdIds */ ] })
// session.id is sent from sender to recipient via Freedom Chat's servers
// receiver
const session = await seald.retrieveEncryptionSession({ sessionId: session.id })
				
			

Now that all participants have a shared symmetric key, they can encrypt messages using a symmetric encryption algorithm. The one we use is AES-CBC combined with an HMAC-SHA256 as detailed in [`sscrypto`’s `SymKey`]

(https://github.com/seald/sscrypto/blob/b360e47eb4e99e8fd363ea880db8191cd3a45af1/src/node/aes.ts#L7).

				
					
// sender
const encryptMessage = async (session, message) => session.encryptMessage(message)

const encryptedMessage = await encryptMessage(session, message)
// encryptedMessage is sent from sender to recipient via Freedom Chat's servers

// receiver
const decryptMessage = async (session, encryptedMessage) => {
	try {
		return await session.decryptMessage(encryptedMessage)
	} catch (error) {
		console.warn("Could not decrypt message: " + error)
		// This may happen if the user is not a legitimate recipient
	}
}
				
			

The same goes for images or videos, only the way they are encoded is different so that the file name can be encrypted as well. An encryption session is rotated any time a user is added or removed to chat, to have a form of forward secrecy.

Checking The Encryption Keys

As mentioned earlier, Freedom Chat applies the TOFU paradigm, but what happens if one does not want to trust at all, at any point, data provided by any third-party (such as Freedom Chat’s servers)? For example, the protocol used to secure web connections, TLS, uses a chain of proof certifying that a given public key indeed belongs to a website. This chain of proof ultimately relies on a third-party authority (called an Certification Authority) we all trust to have checked that the public key is indeed legitimate for that website.

As the goal is to avoid relying on a third party (at all), Freedom Chat implements a peer-to-peer safety mechanism that allows end users to check that the public keys, the symmetric key, and the list of participants to a chat that they retrieved from the servers are indeed legitimate. This mechanism is a QRCode that any participant in a chat can generate and display so that other users can scan it when meeting face-to-face (or over the phone if not in close proximity). It contains the hash of a list of the participants to the chat, a hash of their public key, and a hash of the shared encryption key:

Freedom Chat | Verifiable End-to-end encryption
				
					
const sigchainHashes = {};
await Promise.all(users.map(async (user) => {
    const getRecipientsResult = await seald.utils.getRecipients({ userIds: [user] });
    if (getRecipientsResult.hairlessRecipients.length > 0 || getRecipientsResult.recipients.length !== 1) {
        throw new Error('retrieved invalid recipients');
    }
    const recipientSealdId = getRecipientsResult.recipients[0];
    const sigchainHash = await seald.getSigchainHash({ sealdIds: [recipientSealdId] });
    sigchainHashes[user] = `${recipientSealdId}:${sigchainHash.hash}`;
}));
const stringifiedSigchainHashes = anotherjson.stringify(sigchainHashes);
const totalHash = seald.sscrypto.utils.sha256(Buffer.concat([Buffer.from(stringifiedSigchainHashes, 'utf8'), encryptionSession._sessionSymKey.key, Buffer.from(encryptionSession.sessionId, 'utf8')])



				
			
When all the users in a chat check that they have the same QRCode, they are sure of the following:
 
  • There is no stowaway participant to the conversation who could maliciously read the messages
  • Every participant has the expected sig-chain hash, which means the expected public key (everyone is who they say they are)

 

If verification is done before the first message is sent (select “verify end-to-end encryption” at the top of each chat) any man-in-the-middle attack is entirely prevented.

No Storage of Messages on Our Servers

When all the users in a chat check that they have the same QRCode, they are sure of the following:
 
  • There is no stowaway participant to the conversation who could maliciously read the messages
  • Every participant has the expected sig-chain hash, which means the expected public key (everyone is who they say they are)

 

If verification is done before the first message is sent (select “verify end-to-end encryption” at the top of each chat) any man-in-the-middle attack is entirely prevented.

Freedom Chat End-to-End Encryption Summary

Freedom Chat’s end-to-end encryption ensures that only authorized users can access their conversations, not hackers, app administrators, government institutions, service providers or even nosey tyrants.

If security code verification is done before the first message is sent any man-in-the-middle attack is entirely prevented.

In addition to verifiable end-to-end encryption, we have absolutely no commercial use of user data (unlike WhatsApp, Viber or Telegram) and protect your personal information with the strictest of security protocols. 

Thank you for being a Freedom Chat user. If you have any feedback or see any room for improvement on how we do things, feel free to contact us at feedback@freedomchat.com.