Skip to content

mierenhoop/picomemo

Repository files navigation

Picomemo

This repository contains a compact and portable implementation of the cryptography required for XMPP's OMEMO (E2EE).

Features

  • 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

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.

gen/omemo0.c gen/omemo2.c

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.

Crypto functions

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

Dependencies

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

Usage

Compile libpicomemo0 and libpicomemo2:

$ make lib

Run the tests:

$ make test-omemo test-omemo2

Take a look at the Makefile if you want to have custom build configurations.

API

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)
        }
    }
}

Security

After calling any function of the API, the stack could be zeroized for extra security. This is not done by the library.

WASM

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.

Contributing

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.

License

The code in this repository is licensed under ISC, all dependencies are also permissively licensed.

About

Portable OMEMO implementation in C.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors