This repository contains a compact and portable implementation of the cryptography required for XMPP's OMEMO (E2EE).
-
Portable: besides x86 also runs on WASM, ARM and embedded systems like the ESP32!
-
Compatible with other XMPP clients that support OMEMO.
-
Low amount of code with few dependencies.
-
High performance by using fast crypto when available.
omemo.c contains implementations of X3DH, Double Ratchet and
Protobuf with an API that is specifically tailored to OMEMO. Without
dependencies on (any) libsignal or libolm code.
Both OMEMO 0.3 (eu.siacs.conversations.axolotl) and OMEMO 0.9
(urn:xmpp:omemo:2) are supported. By default OMEMO 0.3 is enabled and
when compiled with -DOMEMO2, OMEMO 0.9 is enabled. That means only one
is active at a time. It is possible to include both versions in a client
by compiling with -DOMEMO_EXPORT=static and directly including
omemo.c.
There are two "backends" for the underlying Curve25519 and EdDSA
cryptography. By default these are provided by
HACL* as an amalgamation in
hacl.c. These are both fast and formally verified.
As an alternative there is the
c25519 library, which is
also included as amalgamation in /c25519.c and /c25519.h. This
library was designed for low-memory systems and is significantly slower
on modern hardware than HACL*.
-
MbedTLS 3.0+
-
C compiler (gcc)
-
For testing & development: GNU Make, docker-compose, Python, openssl, rlwrap, socat, xxd, POSIX commands, ctags-exuberant
Running the tests:
$ make test-omemo
Using this library in your project:
Copy over the source files, omemo.c/h are mandatory and you have to
choose either c25519.c/h or hacl.c/h.
You must link against libmbedcrypto (and/or configure your mbedtls build to only include the required functions).
Refer to omemo.h for function definitions and function-specific
documentation.
To give an understanding how the API can be integrated in a client, here is some pseudocode for a rough overview of how the functions can be used:
API usage in pseudocode
// Implement random callback (required!)
int omemoRandom(p, n) {
getrandom(p, n, 0)
return 0
}
class XmppClient {
struct omemoStore store
Map<(Jid, DeviceId), struct omemoSession> sessions
Setup() {
// Loading/Saving OMEMO store (your bundle of keys)
storebin = ReadFile("store.bin")
if storebin {
> omemoDeserializeStore(storebin, len(storebin), &store)
} else {
// Most calls return an integer, which maps to a OMEMO_E*
// error code (error handling is not done in this example)
> omemoSetupStore(&store)
file = OpenFile("store.bin")
> file.buffer_size = omemoGetSerializedStoreSize(&store)
> omemoSerializeStore(file.buffer, &store)
// Extract keys/ids from the store for publishing your bundle
PublishBundle(&store)
}
// Key should be rotated once every week to month, remember to
// save the store to disk after any changes AND publish your new
// bundle.
> setInterval(() => omemoRotateSignedPreKey(&store), 1*Week)
}
SendEncryptedMessage(to_jid, body) {
// With OMEMO 0.3 you can omit omemoGetMessagePadSize
encrypted_buf = malloc(len(body)+omemoGetMessagePadSize(len(body)))
> omemoEncryptMessage(encrypted_buf, out key_payload, out iv, body,
len(body))
// Get device list via (cached) iq stanza
for device_id in GetDevices(to_jid) {
session = sessions.Get(to_jid, device_id)
if not session {
session = {0}
bundlexml = FetchBundle(to_id, device_id)
> omemoInitiateSession(&session, &store, bundle.spks, bundle.spk, ...)
sessions.Set(to_jid, device_id, session)
}
> omemoEncryptKey(&session, out key_msg, key_payload)
headers.append(MakeXml(key_msg.isprekey, key_msg.p, key_msg.n))
// Save session into some database/file just like the store
> omemoSerializeSession(..., &session)
// Also save the store the store (not shown)
}
SendMessage(MakeXml(headers, encrypted_buf, iv))
}
event GotMessage(msg) {
if msg.is_omemo_encrypted? {
// For decryption, the functions omemoLoadMessageKey and
// omemoStoreMessageKey will be called, you must implement
// them just like omemoRandom at the top level (shown here
// for demonstration purposes.
> int omemoLoadMessageKey(session, key) {
key.mk = skipped_keys[session][key.dh, key.nr]
return Found?(key.mk) ? 0 : 1
}
> int omemoStoreMessageKey(session, key, n) {
if n > MAX_SKIP_KEYS {
return OMEMO_EUSER
}
skipped_keys[session][key.dh, key.nr] = key.mk
}
// Search the XML of the message for our key and get the
// prekey/kex attribute
key, isprekey = FindKeyForOurDevice(msg)
> omemoDecryptKey(&session, &store, out key_payload, isprekey, key)
if isprekey {
// Remove session.usedpk_id from bundle. You should
// actually wait a bit before removing the key.
for (k in store.prekeys)
bzero(k) if k.id == session.usedpk_id
> omemoRefillPreKeys(&store)
PublishBundle(&store)
// Send empty message
SendEncryptedMessage(msg.from, nil)
} else {
struct omemoKeyMessage keymsg = {0}
> omemoHeartbeat(&session, &store, &keymsg)
if msg.n > 0
SendEncryptedMessage(msg.from, keymsg)
}
plaintext = allocate(len(msg.payload))
> omemoDecryptMessage(plaintext, key_payload, msg.iv, msg.payload, len(msg.payload))
// Save session and store here again (not shown), also save
// the skipped keys!
ShowPlaintextMessage(plaintext)
}
}
}
After calling any function of the API, the stack could be zeroized for extra security. This is not done by the library.
This library can be compiled to Web Assembly using emscripten. Both crypto backends are supported. The tests are compiled to WASM and ran in the CI.
The code in this repository is licensed under ISC, all dependencies are also permissively licensed.