xochimilco

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: GPL-3.0 Imports: 17 Imported by: 0

README

Xochimilco

Go Reference Go REUSE status

An implementation of the Signal Protocols X3DH and Double Ratchet, parametrized as specified by OMEMO, XEP-0384. Plus a simple straightforward usable E2E encryption library build on top, named Xochimilco.

The cryptographic primitives, constants and wire formats of both the key agreement and the ratchet follow the OMEMO profile:

  • X3DH over Curve25519 with SHA-256, the "OMEMO X3DH" info string, Ed25519 identity keys and a mandatory one-time prekey.
  • The Double Ratchet with the "OMEMO Root Chain" resp. "OMEMO Message Key Material" HKDF infos, the 0x01/0x02 chain key constants, AES-256-CBC with a 16 byte truncated HMAC-SHA-256 and OMEMOMessage/OMEMOAuthenticatedMessage Protocol Buffers.
  • Payloads are encrypted with a fresh random key, derived by the "OMEMO Payload" HKDF info; only this key and the payload's authentication tag are sent through the ratchet.

Everything above the cryptography stays out of scope, as before. Xochimilco is not an XMPP library: it neither speaks the urn:xmpp:omemo:2 namespace nor handles PEP bundles, device lists or trust management. Its own compact message framing carries the very same key material an OMEMO bundle resp. an OMEMOKeyExchange would.

For both implementation details and examples, take a look at the documentation.

This is based on Alvar Penning's xochimilco, which implements the Signal Protocols with their own parameters; the fork's changes are the OMEMO parametrization outlined above.

Some background, the lake Xochimilco seems to be the last native habitat for the axolotl. This salamander, also called Mexican walking fish, has incredibly self healing abilities. For this reason, the Double Ratchet algorithm was initially named after this animal.

Documentation

Overview

Package xochimilco provides an usable API for end-to-end encrypted communication based on the "Signal Protocol".

The "Signal Protocol" refers to the Extended Triple Diffie-Hellman (X3DH) key agreement protocol paired with the Double Ratchet algorithm. Both are implemented and exposed in this repository's subdirectories, parametrized as specified by OMEMO, XEP-0384[0]. For implementation details please refer there.

Following OMEMO, a message's payload is encrypted with a fresh random key, AES-256-CBC with a truncated HMAC-SHA-256. Only this key next to the payload's authentication tag is passed through the Double Ratchet.

Everything above the cryptography is out of this package's scope. Thus, this is no XMPP library; neither the urn:xmpp:omemo:2 namespace nor PEP bundles, device lists or trust management are implemented. Xochimilco's own message framing carries the same key material as an OMEMO bundle resp. an OMEMOKeyExchange would.

[0] https://xmpp.org/extensions/xep-0384.html

This package is based on Alvar Penning's xochimilco[1], which implements the Signal Protocols with their own parameters.

[1] https://github.com/oxzi/xochimilco
Example
// In this example, Alice and Bob can exchange messages over some chat
// protocol. Furthermore, they already know each other's public key.
alicePub, alicePriv, _ := ed25519.GenerateKey(nil)
bobPub, bobPriv, _ := ed25519.GenerateKey(nil)

alice := Session{
	IdentityKey: alicePriv,
	VerifyPeer: func(peer ed25519.PublicKey) (valid bool) {
		return peer.Equal(bobPub)
	},
}
bob := Session{
	IdentityKey: bobPriv,
	VerifyPeer: func(peer ed25519.PublicKey) (valid bool) {
		return peer.Equal(alicePub)
	},
}

// Alice starts by offering Bob to upgrade the connection.
offerMsg, err := alice.Offer()
if err != nil {
	panic(err)
}
fmt.Printf("A->B\tOFFER\t%s\n", offerMsg)

// Bob acknowledges Alice's offer.
ackMsg, err := bob.Acknowledge(offerMsg)
if err != nil {
	panic(err)
}
fmt.Printf("B-A\tACK\t%s\n", ackMsg)

// Alice evaluates Bob's acknowledgement. This SHOULD be `isEstablished`.
isEstablished, _, _, err := alice.Receive(ackMsg)
if err != nil {
	panic(err)
} else if !isEstablished {
	panic("invalid message")
}

// Now we have an established connection.
// Let's exchange some very important messages.
dataMsgAlice1, err := alice.Send([]byte("hello bob"))
if err != nil {
	panic(err)
}
dataMsgAlice2, err := alice.Send([]byte("how are you?"))
if err != nil {
	panic(err)
}

// Ops, the messages were reorder on the wired.
fmt.Printf("A->B\tDATA\t%s", dataMsgAlice2)
fmt.Printf("A->B\tDATA\t%s", dataMsgAlice1)

_, _, plaintextAlice2, err := bob.Receive(dataMsgAlice2)
if err != nil {
	panic(err)
}
fmt.Printf("B\tRECV\t%s", plaintextAlice2)

_, _, plaintextAlice1, err := bob.Receive(dataMsgAlice2)
if err != nil {
	panic(err)
}
fmt.Printf("B\tRECV\t%s", plaintextAlice1)

// Bob also sends an answer.
dataMsgBob, err := bob.Send([]byte("hej alice!"))
if err != nil {
	panic(err)
}
fmt.Printf("B->A\tDATA\t%s", dataMsgBob)

_, _, plaintextBob, err := alice.Receive(dataMsgBob)
if err != nil {
	panic(err)
}
fmt.Printf("A\tRECV\t%s", plaintextBob)

// Finally, Alice closes her Session...
closeMsg, err := alice.Close()
if err != nil {
	panic(err)
}
fmt.Printf("A->B\tCLOSE\t%s", closeMsg)

// ...and tells Bob to do the same.
_, isClosed, _, err := bob.Receive(closeMsg)
if err != nil {
	panic(err)
} else if !isClosed {
	panic("invalid message")
}

_, err = bob.Close()
if err != nil {
	panic(err)
}

Index

Examples

Constants

View Source
const (

	// Prefix indicates the beginning of an encoded message.
	Prefix string = "!XO!"

	// Suffix indicates the end of an encoded message.
	Suffix string = "!OX!"
)

Variables

This section is empty.

Functions

func DecryptPayload

func DecryptPayload(keyMaterial, ciphertext []byte) (plaintext []byte, err error)

DecryptPayload based on the key material received through the Double Ratchet.

This is the counterpart to EncryptPayload: after a recipient device's own DoubleRatchet.Decrypt has recovered the key material from its per-device wrapped key, DecryptPayload recovers the plaintext from the shared payload ciphertext.

func DecryptPayloadV1 added in v0.1.0

func DecryptPayloadV1(keyMaterial, iv, ciphertext []byte) (plaintext []byte, err error)

DecryptPayloadV1 is the counterpart to EncryptPayloadV1: after a recipient device's own DoubleRatchet.Decrypt has recovered the key material and the wire has supplied iv and ciphertext, this recovers the plaintext.

func EncryptPayload

func EncryptPayload(plaintext []byte) (keyMaterial, ciphertext []byte, err error)

EncryptPayload with a fresh random key, resulting in the ciphertext and the key material to be sent through the Double Ratchet.

The key material is the concatenation of the 32 bytes key and the ciphertext's 16 bytes authentication tag.

This is exported so that callers needing OMEMO's multi-recipient fan-out - one shared payload ciphertext, individually wrapped per recipient device via that device's own Double Ratchet - can encrypt the payload once and pass the resulting key material to each device's DoubleRatchet.Encrypt in turn, instead of going through the single-peer Session type.

func EncryptPayloadV1 added in v0.1.0

func EncryptPayloadV1(plaintext []byte) (keyMaterial, iv, ciphertext []byte, err error)

EncryptPayloadV1 encrypts plaintext with a fresh AES-128-GCM key/IV pair, as legacy OMEMO (eu.siacs.conversations.axolotl) demands.

The returned keyMaterial (key || GCM tag) is what must be sent through the Double Ratchet; iv is sent unencrypted in the wire's <iv/> element; ciphertext is the wire's <payload/> content, both base64 encoded by the caller.

Types

type Session

type Session struct {
	// IdentityKey is this node's private Ed25519 identity key.
	//
	// This will only be used within the X3DH key agreement protocol. The other
	// party might want to verify this key's public part.
	IdentityKey ed25519.PrivateKey

	// VerifyPeer is a callback during session initialization to verify the
	// other party's public key.
	//
	// To determine when a key is correct is out of Xochimilco's scope. The key
	// might be either exchanged over another secure channel or a trust on first
	// use (TOFU) principle might be used.
	VerifyPeer func(peer ed25519.PublicKey) (valid bool)
	// contains filtered or unexported fields
}

Session between two parties to exchange encrypted messages.

Each party creates a new Session variable configured with their private long time identity key and a function callback to verify the other party's public identity key.

The active party must start by offering to "upgrade" the current channel (Offer). Afterwards, the other party must confirm this step (Acknowledge). Once the first party finally receives the acknowledgement (Receive), the connection is established.

Now both parties can create encrypted messages directed to the other (Send). Furthermore, the Session can be closed again (Close). Incoming messages can be inspected and the payload extracted, if present (Receive).

func (*Session) Acknowledge

func (sess *Session) Acknowledge(offerMsg string) (ackMsg string, err error)

Acknowledge to establish an encrypted Session.

This method MUST be called by the passive party (Bob) with the active party's (Alice's) offer message. The created acknowledge message MUST be send back.

At this point, this passive part is able to send and receive messages.

func (*Session) Close

func (sess *Session) Close() (closeMsg string, err error)

Close this Session and tell the other party to do the same.

This resets the internal state. Thus, the same Session might be reused.

func (*Session) Offer

func (sess *Session) Offer() (offerMsg string, err error)

Offer to establish an encrypted Session.

This method MUST be called initially by the active resp. opening party (Alice) once. The other party will hopefully Acknowledge this message.

func (*Session) Receive

func (sess *Session) Receive(msg string) (isEstablished, isClosed bool, plaintext []byte, err error)

Receive an incoming message.

All messages except the passive party's initial offer message MUST be passed to this method. The multiple return fields indicate this message's kind.

If the active party receives its first (acknowledge) message, this Session will be established; isEstablished. If the other party has signaled to close the Session, isClosed is set. This Session MUST then also be closed down. In case of an incoming encrypted message, the plaintext field holds its decrypted plaintext value. Of course, there might also be an error.

func (*Session) Send

func (sess *Session) Send(plaintext []byte) (dataMsg string, err error)

Send a message to the other party. The given plaintext byte array will be embedded in an encrypted message.

This method is allowed to be called after the initial handshake, Offer resp. Acknowledge.

Directories

Path Synopsis
Package doubleratchet implements the Double Ratchet Algorithm as profiled by OMEMO, XEP-0384 version 0.9[1].
Package doubleratchet implements the Double Ratchet Algorithm as profiled by OMEMO, XEP-0384 version 0.9[1].
internal
Package legacysignal implements the pre-standardization "legacy OMEMO" wire protocol (eu.siacs.conversations.axolotl), as actually spoken by real clients (Conversations, Dino, Gajim, ChatSecure) - not the conceptually-similar but byte-incompatible XEP-0384 (OMEMO 2) profile implemented by xochimilco's x3dh/doubleratchet packages.
Package legacysignal implements the pre-standardization "legacy OMEMO" wire protocol (eu.siacs.conversations.axolotl), as actually spoken by real clients (Conversations, Dino, Gajim, ChatSecure) - not the conceptually-similar but byte-incompatible XEP-0384 (OMEMO 2) profile implemented by xochimilco's x3dh/doubleratchet packages.
Package x3dh implements the Extended Triple Diffie-Hellman (X3DH) key agreement protocol as profiled by OMEMO, XEP-0384 version 0.9[4].
Package x3dh implements the Extended Triple Diffie-Hellman (X3DH) key agreement protocol as profiled by OMEMO, XEP-0384 version 0.9[4].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL