E2EE Security v1.5.0
End-to-End Encryption Security Whitepaper
Gen-Cher Lee, Hsuan-Hung Kuo
March 22, 2025
Introduction
This white paper provides a technical overview on the end-to-end encryption (E2EE) protocols and implemented security aspects.
Fig.1: E2EE Security Architecture
E2EE Security provides an in-depth design of E2EE messaging framework extends the Signal protocol[1][2][3][4] and supports both pre-quantum and post-quantum cryptographic primitives. E2EE Security supports asynchronous and out-of-order end-to-end message encryption scheme. It also supports one-to-one messaging and group messaging for registered user with multiple devices. The two crucial security properties are provided:
-
End-to-end encryption
Only sender and recipient (and not even the server) can decrypt the content.
-
Forward secrecy
Past sessions are protected against future compromises of keys or passwords.
The extensible software architecture [Fig.1] implemented by E2EE Security has been released as open source project [19].
Cipher Suites
A cipher suite in E2EE Security is a software interface constructed by the following cryptographic functions. E2EE Security provides an implementation that utilizes the curve25519-donna[10], mbed TLS [11] library, and PQClean [21].
-
get_crypto_param
Get the parameters of the cipher suite.
-
asym_key_gen
Generate a random key pair that will be used to calculate shared secret keys.
-
sign_key_gen
Generate a random key pair that will be used to generate or verify a signature.
-
ss_key_gen
Calculate shared secret key.
-
encrypt
Encrypt a given plaintext.
-
decrypt
Decrypt a given ciphertext.
-
sign
Sign a message.
-
verify
Verify a signature with a given message.
-
hkdf
HMAC-based key derivation function.
-
hmac
Keyed-Hashing for message authentication.
-
hash
Create a hash.
The default cipher suite in the E2EE Security is E2EE_CIPHER_DILITHIUM5_KYBER1024_AES256_GCM_SHA2_256, which contains Kyber[13], Dilithium5[18], AES256-GCM, SHA2-256.
Key Size
(bytes) | public key | secret key | ciphertext |
---|---|---|---|
kyber512 | 800 | 1632 | 768 |
kyber768 | 1184 | 2400 | 1088 |
kyber1024 | 1568 | 3168 | 1568 |
(bytes) | secret key | public key | signature |
---|---|---|---|
Dilithium2 | 2528 | 1312 | 2420 |
Dilithium3 | 4000 | 1952 | 3293 |
Dilithium5 | 4864 | 2592 | 4595 |
Plugins
E2EE Security implements a plugin interface to achieve module flexibility for engaging variant application platforms. There are four kinds of plugin handlers [Fig.2]:
Fig.2: E2EE Security plugin interface
-
Common handler
A common handler provides a set of platform dependent functions for generating time stamp, random number, and universally unique identifier (UUID).
-
Event handler
An event handler is used to receive events form E2EE Security while performing E2EE protocol request and processing protocol messages from server. User application can make use of this efficient notification mechanism to catch the changes of states that are maintained by E2EE Security.
-
Database handler
A database handler is provided to help E2EE Security keeping data persistency. The state accessibility of user account, one-to-one sessions, and group sessions are finely implemented through a range of database functions.
-
Protocols handler
The fourth handler is designated to provide a layer of protocol transportation that helps E2EE Security forwarding the request messages to E2EE Server. The response message of each request is then propagated back to E2EE Security to maintain the states of account and sessions. E2EE Security will use the functional interface of database handler to maintain data persistency of affected sessions and account.
Addressing
To specify an end point address for end-to-end encryption, E2EE Security provides an E2eeAddress struct to represent user address or group address as shown in [Fig.3]. An E2eeAddress with PeerUser type is obtained from E2EE server after user registration. The PeerUser data is specified by a user_id that is created uniquely by the server and a device_id that is provided by a user. An alternative E2eeAddress with PeerGroup type is obtained from E2EE server after group creation. The PeerGroup data is specified by a uniquely assigned group_id from server. The uniqueness of user_id and group_id is assured in the scope of the same server domain while the uniqueness of device_id is kept by user application.
Fig.3: E2eeAddress struct
Account
An account keeps user’s address and three types of keys that are used by E2EE schemes. A complete set of keys include a long-term key-pair (IdentityKey), a mid-term key-pair (SignedPreKey), and a bunch (100 as default) of one-time used key-pairs (OneTimePreKey) [Fig.4]. On the stage of user registration, the public part of this set of key-pairs will be uploaded to E2EE server to help together with other peers establish sessions for messaging.
Moreover, the SignedPreKey has a special time-to-live (ttl) attribute that helps remember the the next renew time. E2EE Security implements 7 days as the default renewal time interval. On each begin time of E2EE Security activation, the module will check this “ttl”. If it is reached then the “publish signed pre-key” protocol will be requested to submit the public part of newly generated signed pre-key. E2EE server will update the key and use it subsequently to provide clients with PreKeyBundle for creating a new outbound session.
Fig.4: Account struct
Pre-Key Bundle
Before a user application can build an outbound session, the “get pre-key bundle” protocol will be used to download the data set that encloses pre-key bundles [Fig.5]. To get the pre-key bundles of a peer user with given user_id, E2EE server will gather a list of pre-key bundles with which is related to each device_id of the same user_id. A PreKeyBundle just collects the public part of an identity key, a signed pre-key and an one-time pre-key. The E2EE server will remove the used one-time pre-keys after sending the collected pre-key bundles as a response.
Fig.5: PreKeyBundle struct
Session
A Session struct [Fig.6] is used to encapsulate the states of one-to-one messaging that will be changed on each encryption or decryption. A session can be used for handling inbound messages or outbound messages alternatively by setting-up the “from” and “to” address attribute. An outbound session is used to send one-to-one encryption message to remote peer, while an inbound session is used to decrypt the incoming one-to-one message received from remote peer. Specially, An outbound session uses the attribute “responded” as a lock that will be enabled if AcceptMsg is received and complete the shared key calculation.
Each session has a “ratchet” attribute with Ratchet struct that maintains the ratchet states [3] for either inbound or outbound usage. If a ratchet is used for managing outbound session, then it will be operated with a sender chain that has ratchet_key to assign that an outbound message belongs to this chain, and a “ratchet” attribute to generate message key for encrypting outbound message. On the other hand, if a ratchet is used for managing inbound session, it will be operated with a receiver chain that has a “ratchet_key_public” attribute to identify the inbound message belongs to this chain, and a “chain_key” attribute to generate message key to decrypt inbound message. The skipped messages chain helps maintain the message key that is skipped while an inbound session is performing decryption task over receiver chain. Moreover, each chain has a max chan index 512 as default by E2EE Security. If an outbound session with ratchet of sender chain reaches the limit, a new outbound session will be built as a replacement by using a new PreKeyBundle provided by server.
Fig.6: Session Struct
Group Session
A GroupSession struct [Fig.7] is used to encapsulate the states of group messaging that will be changed on each encryption or decryption. A group session can be used for handling inbound group messages or outbound group messages. In the case of outbound group session, the “signature_private_key” attribute will be created while an inbound group session only make use of “signature_public_key”.
An outbound group session is created after a success request of “create group” protocol and returning a unique group address. Then a GroupPreKeyBundle message will be packed as the payload of a Plaintext type message and delivered to each group member through one-to-one session introduced previously. E2EE server then help forwarding the one-to-one message to recipient. Each group member can build inbound group session after processing the decrypted plaintext with “group_pre_key” payload. If some one-to-one outbound is not ready for sending message, E2EE Security will keep the data in database, and the saved “group_pre_key_plaintext” will be resent automatically after a respective AcceptMsg has been received and successfully create the outbound session.
The group members can be altered by requesting “add group members” and “remove group member” protocol. E2EE Security will automatically rebuild the outbound group session if the group members were changed. The inbound group session of each group member will also be rebuilt as a result.
Fig.7: GroupSession struct
Cryptographic Algorithms
Abbreviations
-
: base key
-
: chain key
-
: ciphertext
-
: decrypt message x with key y using AES256 with GCM mode
-
: Takes as input a ciphertext C and secret key and outputs K
-
: encrypt message x with key y using AES256 with GCM mode
-
: Takes as input a public key pk and outputs a ciphertext C and the encapsulated key K
-
: HKDF with SHA-256 with input key material IKM, salt, and info
-
: HMAC with SHA-256 with the key and the input
-
: identity key pair
-
: message key
-
: one-time pre-key pair
-
: plaintext
-
: ratchet key pair
-
: root key
-
: sign message x with private key y and output the signature sig
-
: signed pre-key pair
-
: shared secret key
-
: signature private key
-
: signature public key
-
: verify the signature sig with the public key k
Algorithms
PQKA
The key agreement process cannot complete at Alice’s side alone in the case of applying post quantum cryptographic (PQC) primitives [12][13][14][15] that mainly work with key encapsulation mechanisms (KEM) [16][17][18]. The flow for calculating the shared key for both Alice and Bob is altered by E2EE Security [Fig.8]. An invite message is sent on creating a new outbound session. The outbound session is not able to send encrypted messages before receiving an accept message and completing the calculation of shared key. E2EE Security implements “invite” and “accept” protocols as a compromise to enable PQKA to work in a uniform data flow for post quantum cryptographic primitives.
Fig.8: Invite and accept protocol
To build a new outbound session, Alice first acquires Bob's pre-key bundle from the server, then performs the following steps:
-
Verify(Sig, )
-
Generate the base key .
-
Start calculating the shared secrets.
Calculate (c2, k2) .
Calculate (c3, k3) .
Calculate (c4, k4) .
-
Send InviteMsg to Bob with pre_shared_input_list: c2, c3, c4.
When Bob receives the InviteMsg from Alice, perform the following steps:
-
Calculate the shared secrets
Calculate (c1, k1) .
Calculate k2 ← Decaps(, c2).
Calculate k3 ← Decaps(, c3).
Calculate k4 ← Decaps(, c4).
secret(128 bytes) = k1 || k2 || k3 || k4
sk(64 bytes) = HKDF(secret, salt[32]=0, info=“ROOT”)
-
Send AcceptMsg with encaps_ciphertext, which is c1.
After Alice receives the AcceptMsg from Bob, she will complete her outbound session by:
-
Calculate Decaps(, c1) where c1 is obtained from the encaps_ciphertext of received AcceptMsg.
-
secret(128 bytes) = k1 || k2 || k3 || k4
-
sk(64 bytes) = HKDF(secret, salt[32]=0, info=“ROOT”).
One2one message
The double ratchet algorithm[3], comprising the asymmetric ratchet and the symmetric ratchet, is employed for sending One2one messages. After the session is created or receiving the other's message, we use the asymmetric ratchet to send a message. On the other hand, we use the symmetric ratchet to send a message after sending some messages to the other.
Asymmetric Ratchet
To encrypt a message, we do the following steps:
-
RK(32 bytes) = prefix 32 bytes of sk
-
Calculate (, secret_input) on Alice's side or (, secret_input) on Bob's side.
-
Next sk(64 bytes)
= HKDF(secret_input, salt=RK, info="RATCHET")
= next RK(32 bytes) || sender_chain_key(32 bytes)
-
mk(48 bytes)
= HKDF(sender_chain_key, salt[32]=0, info="MessageKeys")
-
C = Enc(P, mk)
-
Send C and to Bob or send C and to Alice.
To decrypt a message, we do the following steps:
-
RK(32 bytes) = prefix 32 bytes of sk
-
secret_input = Decaps(, ) on Alice's side or Decaps(, ) on Bob's side.
-
Next sk(64 bytes)
= HKDF(secret_input, salt=RK, info="RATCHET")
= next RK(32 bytes) || receiver_chain_key(32 bytes)
-
mk(48 bytes)
= HKDF(receiver_chain_key, salt[32]=0, info="MessageKeys")
-
P = Dec(C, mk)
Symmetric Ratchet
To perform symmetric ratchet, we generate a new chain key by using the current chain key: . Next, generate the message key by . We then encrypt the message with .
Group message
Secure group messaging protocols have been the focus of much recent cryptographic work. We referred to the design of DCGKA[20] to ensure the following properties:
-
Decentralization: Servers may still optionally be used, but they are trusted less.
-
Efficiency: The group creation and group membership synchronization have linear complexity.
-
Authentication: Only members can send messages to the group, and the sender of a message cannot be forged.
-
Confidentiality: The message sent by a group member can only be decrypted by users who are members of the group at the time the message is sent.
-
Integrity: Messages cannot be undetectably modified by anyone other than the sender.
-
Forward Secrecy: After decrypting a group message, an adversary who compromises the private state of the member can't decrypt it.
-
Post-Compromise Security (PCS): If a group member is compromised by an adversary but still can send messages, the adversary can't decrypt messages from any group member who has completed the PCS update.
Create group
Each group member creates an outbound group session for encrypting and sending group message. On the other hand, the other group members create inbound group session with respect to the outbound group session for decrypting received group message [Fig.9].
-
The group creator creates an outbound group session by generating a random seed secret(ss). The creator then combines the seed secret with his or her own identity public key to generate a chain key with HKDF.
-
The group creator then send the seed secret to each group member by using one-to-one session. Each group member can build their own outbound group session by using the seed secret.
Fig.9: Group session creation
Also, the server needs to send the group members’ identity public key to all the other group members so that every group member can generate the corresponding inbound group sessions [Fig.10].
Fig.10: Chain key generation
To encrypt and send outbound group message, Alice uses the established outbound group session and performs the following steps [Fig.11]:
-
mk = HKDF(ck)
-
C = Enc(P, mk)
-
Sig = Sign(C, ik_priv)
-
Send (C, Sig) to each group member
Alice uses the outbound group session to ratchet ck for the next encryption.
Fig.11: Group message delivery
To decrypt a received inbound group message, Bob and other group members use the established inbound group session and perform the following steps:
-
Verify(Sig, ik_pub)
-
mk = HKDF(ck)
-
P = Dec(C, mk)
Each group member uses their own inbound group session to ratchet ck for the next decryption.
Add members
When a new group member is added to the group, the other old group members need to send their current chain key to the new group member via one-to-one session so that this new group member can create corresponding inbound group sessions. On the other hand, the new group member creates his or her outbound group session with the inviter’s(the one who invites the new group member to join the group) chain key. Since all of the old group members have the inviter’s chain key, they can create the inbound group session that can be used to decrypt the new group member’s group message [Fig.12].
Fig.12: Add a group member
Remove members
When some group members are removed, the group member who makes the changed event will generate a new seed secret and send to other remained group members via one-to-one session, so that all of the remained group members will rebuild group sessions, including outbound and inbound. As a result, all outbound and inbound group sessions will be renewed, and the removed group members has no information about the updated group sessions.
E2EE Protocols
E2EE Security offers offers various request-response protocols and aids in managing server-sent messages [Fig.13]. The request-response protocols provide a direct control and message exchange mechanism for interacting with the E2EE server, enabling better integration with user applications. On the other hand, the server-sent events protocol efficiently notifies E2EE Security and keeps the states of accounts and sessions updated with the E2EE messaging schemes.
Fig.13: E2EE protocols