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. Be careful to compile all of your code using
omemo.h with -DOMEMO2 if omemo.c is also compiled with it.
These files and their headers are generated by splitting omemo.c and its header file in two, resolving the OMEMO2 define. All identifiers are also modified to add the OMEMO version number. This is done to make dynamic linking both possible.
The CI verifies that these generated files are up-to-date.
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. This
library was designed for low-memory systems and is significantly slower
on modern hardware than HACL*.
One of the following two must be available at runtime:
-
MbedTLS 3.0+
-
OpenSSL
For compiling the shared libraries:
-
C compiler (gcc)
-
GNU Make 4.2+
-
Lua 5.4, or 5.1+
-
POSIX commands
For testing & development: docker-compose, Python, openssl, rlwrap, socat, xxd, ctags-exuberant
Compile libpicomemo0 and libpicomemo2:
$ make libRun the tests:
$ make test-omemo test-omemo2Take a look at the Makefile if you want to have custom build configurations.
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
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 LoadMessageKey(session, key) {
key.mk = skipped_keys[session][key.dh, key.nr]
return Found?(key.mk) ? 0 : 1
}
int StoreMessageKey(session, key, n) {
if n > MAX_SKIP_KEYS {
return OMEMO_EUSER
}
skipped_keys[session][key.dh, key.nr] = key.mk
}
// This should usually be done once at the beginning of your
// program
> omemoSetCallbacks(LoadMessageKey, StoreMessageKey, NULL);
// 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 WebAssembly using emscripten. Both crypto backends are supported, and since HACL* is faster you should use that one. In the CI, the tests also run in WASM environment.
Open issues and pull requests at github.com/mierenhoop/picomemo.
Alternatively you can send an email or XMPP message to the addresses listed at mnhp.nl/contact, optionally with a git patch.
The code in this repository is licensed under ISC, all dependencies are also permissively licensed.