diff --git a/Dockerfile b/Dockerfile index c3858e9..413ddbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.19-bookworm AS base RUN apt-get update -run apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git +run apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git libomemo-c-dev FROM base AS tdlib diff --git a/Makefile b/Makefile index b1facea..a6db344 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,17 @@ TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" VERSION := "v2.0.0-dev" MAKEOPTS := "-j4" +# GOTAGS=signal_legacy builds the OMEMO cgo bindings against the pre-fork +# libsignal-protocol-c instead of libomemo-c (e.g. on Debian bullseye, which +# only packages the former) - legacy/siacs OMEMO only, no modern OMEMO 1/2. +GOTAGS := + all: mkdir -p release - go build -ldflags "-X main.commit=${COMMIT}" -o release/telegabber + go build -ldflags "-X main.commit=${COMMIT}" -tags "${GOTAGS}" -o release/telegabber test: - go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger + go test -tags "${GOTAGS}" -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger lint: $(GOPATH)/bin/golint ./... diff --git a/README.md b/README.md index ae80f4a..a0c4273 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ Prerequisites: Go links the binary against the `glibc` version present in the sy * Build TDLib according to [TDLib build instructions](https://tdlib.github.io/td/build.html). Roll back to commit 8d7bda00a535d1eda684c3c8802e85d69c89a14a if the compatibility got broken. +* Install a Signal-protocol C library for OMEMO support - one of: + * [libomemo-c](https://github.com/dino/libomemo-c), the Dino project's fork that supports both legacy/siacs OMEMO and modern OMEMO (1/2). Packaged on Debian bookworm and later (and correspondingly recent Ubuntu/derivatives) as `libomemo-c-dev` - `apt install libomemo-c-dev` is all you need. This is the default Telegabber builds against. + * [libsignal-protocol-c](https://github.com/signalapp/libsignal-protocol-c), the pre-fork original, on distros that only package that one (e.g. Debian bullseye: `apt install libsignal-protocol-c-dev`). It only supports legacy/siacs OMEMO, not modern OMEMO 1/2. Build with `make GOTAGS=signal_legacy` (or `go build -tags signal_legacy ...`) to link against it instead. + + Neither package available for your distro? Build either one from source the same way: + ``` + git clone https://github.com/dino/libomemo-c # or .../signalapp/libsignal-protocol-c + cd libomemo-c && mkdir build && cd build + cmake -DCMAKE_BUILD_TYPE=Release .. + make && sudo make install + ``` + libomemo-c additionally requires `libprotobuf-c` (dev headers + library) to build. Either way, Telegabber's cgo bindings locate whichever one you chose via `pkg-config` (`libomemo-c` or `libsignal-protocol-c`, matching the build tag), so make sure that succeeds afterward - install to a prefix `pkg-config` already searches (the distro packages do this automatically; a manual `make install` defaults to `/usr/local`, or point `PKG_CONFIG_PATH` at wherever the `.pc` file ended up, typically `/lib/pkgconfig`). + * Install Go (tested with 1.13, but may work with earlier versions too). * Open the source dir in a new shell (to make sure that `$GOPATH` works) and run `make`. Dependencies will be installed automatically. diff --git a/e2ee/omemo/libsignal/address.go b/e2ee/omemo/libsignal/address.go new file mode 100644 index 0000000..2258eee --- /dev/null +++ b/e2ee/omemo/libsignal/address.go @@ -0,0 +1,45 @@ +package libsignal + +/* +#include +*/ +import "C" + +import ( + "runtime" + "unsafe" +) + +// Address identifies a Signal Protocol session peer: a logical recipient +// name (for OMEMO, a bare JID) plus a numeric device ID. +type Address struct { + Name string + DeviceID uint32 +} + +// withCAddress converts addr to a C signal_protocol_address and invokes fn +// with a pointer to it. The pointer - and the memory its name field borrows +// from addr.Name - is only valid for the duration of fn; it must not be +// retained past fn's return. +func withCAddress(addr Address, fn func(*C.signal_protocol_address)) { + nameBytes := []byte(addr.Name) + var namePtr *C.char + if len(nameBytes) > 0 { + namePtr = (*C.char)(unsafe.Pointer(&nameBytes[0])) + } + cAddr := C.signal_protocol_address{ + name: namePtr, + name_len: C.size_t(len(nameBytes)), + device_id: C.int32_t(addr.DeviceID), + } + fn(&cAddr) + runtime.KeepAlive(nameBytes) +} + +// addressFromC copies a C signal_protocol_address into a Go Address. +func addressFromC(addr *C.signal_protocol_address) Address { + return Address{ + Name: C.GoStringN(addr.name, C.int(addr.name_len)), + DeviceID: uint32(addr.device_id), + } +} diff --git a/e2ee/omemo/libsignal/buffer.go b/e2ee/omemo/libsignal/buffer.go new file mode 100644 index 0000000..ebc040d --- /dev/null +++ b/e2ee/omemo/libsignal/buffer.go @@ -0,0 +1,50 @@ +package libsignal + +/* +#include +*/ +import "C" + +import "unsafe" + +// bytesToBuffer copies a Go byte slice into a newly allocated signal_buffer. +// Ownership of the returned buffer transfers to whichever C API consumes it +// (matching the library's own buffer-ownership convention documented in +// signal_protocol.h). +func bytesToBuffer(b []byte) *C.signal_buffer { + var ptr *C.uint8_t + if len(b) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&b[0])) + } + return C.signal_buffer_create(ptr, C.size_t(len(b))) +} + +// bufferToBytes copies the contents of a signal_buffer into a new Go byte +// slice. It does not free or otherwise take ownership of buf. +func bufferToBytes(buf *C.signal_buffer) []byte { + if buf == nil { + return nil + } + n := C.signal_buffer_len(buf) + if n == 0 { + return []byte{} + } + return C.GoBytes(unsafe.Pointer(C.signal_buffer_const_data(buf)), C.int(n)) +} + +// freeBuffer frees a signal_buffer allocated by the library. +func freeBuffer(buf *C.signal_buffer) { + if buf != nil { + C.signal_buffer_free(buf) + } +} + +// cBytesToGo copies a raw C byte pointer + length into a new Go byte slice. +// Used for reading uint8_t*/size_t pairs (as opposed to signal_buffer*) out +// of C structures, e.g. session_signed_pre_key signatures. +func cBytesToGo(data *C.uint8_t, length C.size_t) []byte { + if data == nil || length == 0 { + return nil + } + return C.GoBytes(unsafe.Pointer(data), C.int(length)) +} diff --git a/e2ee/omemo/libsignal/callbacks.go b/e2ee/omemo/libsignal/callbacks.go new file mode 100644 index 0000000..4ec4dac --- /dev/null +++ b/e2ee/omemo/libsignal/callbacks.go @@ -0,0 +1,293 @@ +package libsignal + +/* +#include +*/ +import "C" + +import ( + "runtime/cgo" + "unsafe" +) + +// storeFromUserData recovers the Store bound to a StoreContext from the +// cgo.Handle round-tripped through the vtable's void *user_data (see +// store.go's NewStoreContext, which is the only place that creates such a +// handle). +func storeFromUserData(userData unsafe.Pointer) (Store, bool) { + if userData == nil { + return nil, false + } + s, ok := cgo.Handle(uintptr(userData)).Value().(Store) + return s, ok +} + +// ---- identity key store ---- + +//export go_get_identity_key_pair +func go_get_identity_key_pair(publicData, privateData **C.signal_buffer, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + pub, priv, err := store.GetIdentityKeyPair() + if err != nil { + return C.int(ErrUnknown) + } + *publicData = bytesToBuffer(pub) + *privateData = bytesToBuffer(priv) + return C.SG_SUCCESS +} + +//export go_get_local_registration_id +func go_get_local_registration_id(userData unsafe.Pointer, registrationID *C.uint32_t) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + id, err := store.GetLocalRegistrationID() + if err != nil { + return C.int(ErrUnknown) + } + *registrationID = C.uint32_t(id) + return C.SG_SUCCESS +} + +//export go_save_identity +func go_save_identity(address *C.signal_protocol_address, keyData *C.uint8_t, keyLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.SaveIdentity(addressFromC(address), cBytesToGo(keyData, keyLen)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_is_trusted_identity +func go_is_trusted_identity(address *C.signal_protocol_address, keyData *C.uint8_t, keyLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + trusted, err := store.IsTrustedIdentity(addressFromC(address), cBytesToGo(keyData, keyLen)) + if err != nil { + return C.int(ErrUnknown) + } + if trusted { + return 1 + } + return 0 +} + +//export go_identity_destroy +func go_identity_destroy(userData unsafe.Pointer) {} + +// ---- pre key store ---- + +//export go_load_pre_key +func go_load_pre_key(record **C.signal_buffer, preKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + data, found, err := store.LoadPreKey(uint32(preKeyID)) + if err != nil { + return C.int(ErrUnknown) + } + if !found { + return C.int(ErrInvalidKeyID) + } + *record = bytesToBuffer(data) + return C.SG_SUCCESS +} + +//export go_store_pre_key +func go_store_pre_key(preKeyID C.uint32_t, record *C.uint8_t, recordLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.StorePreKey(uint32(preKeyID), cBytesToGo(record, recordLen)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_contains_pre_key +func go_contains_pre_key(preKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return 0 + } + if store.ContainsPreKey(uint32(preKeyID)) { + return 1 + } + return 0 +} + +//export go_remove_pre_key +func go_remove_pre_key(preKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.RemovePreKey(uint32(preKeyID)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_pre_key_destroy +func go_pre_key_destroy(userData unsafe.Pointer) {} + +// ---- signed pre key store ---- + +//export go_load_signed_pre_key +func go_load_signed_pre_key(record **C.signal_buffer, signedPreKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + data, found, err := store.LoadSignedPreKey(uint32(signedPreKeyID)) + if err != nil { + return C.int(ErrUnknown) + } + if !found { + return C.int(ErrInvalidKeyID) + } + *record = bytesToBuffer(data) + return C.SG_SUCCESS +} + +//export go_store_signed_pre_key +func go_store_signed_pre_key(signedPreKeyID C.uint32_t, record *C.uint8_t, recordLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.StoreSignedPreKey(uint32(signedPreKeyID), cBytesToGo(record, recordLen)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_contains_signed_pre_key +func go_contains_signed_pre_key(signedPreKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return 0 + } + if store.ContainsSignedPreKey(uint32(signedPreKeyID)) { + return 1 + } + return 0 +} + +//export go_remove_signed_pre_key +func go_remove_signed_pre_key(signedPreKeyID C.uint32_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.RemoveSignedPreKey(uint32(signedPreKeyID)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_signed_pre_key_destroy +func go_signed_pre_key_destroy(userData unsafe.Pointer) {} + +// ---- session store ---- + +//export go_load_session +func go_load_session(record, userRecord **C.signal_buffer, address *C.signal_protocol_address, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + data, found, err := store.LoadSession(addressFromC(address)) + if err != nil { + return C.int(ErrUnknown) + } + if !found { + return 0 + } + *record = bytesToBuffer(data) + return 1 +} + +//export go_get_sub_device_sessions +func go_get_sub_device_sessions(sessions **C.signal_int_list, name *C.char, nameLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + deviceIDs, err := store.GetSubDeviceSessions(C.GoStringN(name, C.int(nameLen))) + if err != nil { + return C.int(ErrUnknown) + } + list := C.signal_int_list_alloc() + if list == nil { + return C.int(ErrNoMemory) + } + for _, id := range deviceIDs { + C.signal_int_list_push_back(list, C.int(id)) + } + *sessions = list + return C.int(len(deviceIDs)) +} + +//export go_store_session +func go_store_session(address *C.signal_protocol_address, record *C.uint8_t, recordLen C.size_t, userRecord *C.uint8_t, userRecordLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.StoreSession(addressFromC(address), cBytesToGo(record, recordLen)); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +//export go_contains_session +func go_contains_session(address *C.signal_protocol_address, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return 0 + } + if store.ContainsSession(addressFromC(address)) { + return 1 + } + return 0 +} + +//export go_delete_session +func go_delete_session(address *C.signal_protocol_address, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + if err := store.DeleteSession(addressFromC(address)); err != nil { + return C.int(ErrUnknown) + } + return 1 +} + +//export go_delete_all_sessions +func go_delete_all_sessions(name *C.char, nameLen C.size_t, userData unsafe.Pointer) C.int { + store, ok := storeFromUserData(userData) + if !ok { + return C.int(ErrUnknown) + } + count, err := store.DeleteAllSessions(C.GoStringN(name, C.int(nameLen))) + if err != nil { + return C.int(ErrUnknown) + } + return C.int(count) +} + +//export go_session_destroy +func go_session_destroy(userData unsafe.Pointer) {} diff --git a/e2ee/omemo/libsignal/capability_legacy.go b/e2ee/omemo/libsignal/capability_legacy.go new file mode 100644 index 0000000..f7fac72 --- /dev/null +++ b/e2ee/omemo/libsignal/capability_legacy.go @@ -0,0 +1,8 @@ +//go:build signal_legacy + +package libsignal + +// ModernOMEMOSupported is false under the "signal_legacy" build tag: this +// build links vanilla libsignal-protocol-c, which has no modern OMEMO +// (protocol v4) support at all - only legacy/siacs OMEMO (protocol v3). +const ModernOMEMOSupported = false diff --git a/e2ee/omemo/libsignal/capability_modern.go b/e2ee/omemo/libsignal/capability_modern.go new file mode 100644 index 0000000..cd068f5 --- /dev/null +++ b/e2ee/omemo/libsignal/capability_modern.go @@ -0,0 +1,10 @@ +//go:build !signal_legacy + +package libsignal + +// ModernOMEMOSupported reports whether this build links a library capable +// of modern OMEMO (protocol v4, shared by OMEMO 1/urn:xmpp:omemo:1 and +// OMEMO 2/urn:xmpp:omemo:2), as opposed to only legacy/siacs OMEMO +// (protocol v3). Callers should check this before advertising or +// negotiating anything beyond the legacy namespace. +const ModernOMEMOSupported = true diff --git a/e2ee/omemo/libsignal/cgo_legacy.go b/e2ee/omemo/libsignal/cgo_legacy.go new file mode 100644 index 0000000..08f8d1f --- /dev/null +++ b/e2ee/omemo/libsignal/cgo_legacy.go @@ -0,0 +1,17 @@ +//go:build signal_legacy + +package libsignal + +/* +#cgo pkg-config: libsignal-protocol-c +*/ +import "C" + +// This file (selected by the "signal_legacy" build tag) links against the +// pre-fork libsignal-protocol-c instead of libomemo-c - e.g. on Debian +// bullseye, which packages the former (libsignal-protocol-c-dev) but not +// the latter (libomemo-c-dev only arrived in bookworm). Vanilla +// libsignal-protocol-c only ever produces/consumes protocol v3 (legacy/ +// siacs OMEMO) messages; see capability_legacy.go, version_legacy.go, +// deserialize_legacy.go, and signedprekey_legacy.go for the resulting +// reduced-capability shims. diff --git a/e2ee/omemo/libsignal/cgo_modern.go b/e2ee/omemo/libsignal/cgo_modern.go new file mode 100644 index 0000000..45dabb0 --- /dev/null +++ b/e2ee/omemo/libsignal/cgo_modern.go @@ -0,0 +1,19 @@ +//go:build !signal_legacy + +package libsignal + +/* +#cgo pkg-config: libomemo-c +*/ +import "C" + +// This file (selected by the absence of the "signal_legacy" build tag, +// the default) links against libomemo-c, the Dino project's fork of +// libsignal-protocol-c that additionally supports modern OMEMO (protocol +// v4, shared by OMEMO 1 and OMEMO 2) alongside legacy/siacs OMEMO +// (protocol v3). See cgo_legacy.go for the alternative. +// +// The #cgo pkg-config directive above supplies the CFLAGS/LDFLAGS for +// every file in this package, per Go's cgo rules (such directives are +// package-wide, not per-file) - every other file just #includes the +// headers it needs without repeating this directive. diff --git a/e2ee/omemo/libsignal/cipher.go b/e2ee/omemo/libsignal/cipher.go new file mode 100644 index 0000000..225e63d --- /dev/null +++ b/e2ee/omemo/libsignal/cipher.go @@ -0,0 +1,144 @@ +package libsignal + +/* +#include +#include +#include +*/ +import "C" + +import ( + "runtime" + "unsafe" +) + +// Ciphertext message types (protocol.h), discriminating what +// CiphertextMessage.Serialized holds. +const ( + // CiphertextSignalType is an ordinary ratchet message; a session must + // already exist to decrypt one. + CiphertextSignalType = C.CIPHERTEXT_SIGNAL_TYPE + // CiphertextPreKeyType is a prekey message that also establishes the + // session on the receiving end, via X3DH. + CiphertextPreKeyType = C.CIPHERTEXT_PREKEY_TYPE +) + +// CiphertextMessage is an encrypted message ready to be embedded in a +// stanza. Type distinguishes an ordinary ratchet message from a prekey +// message - the caller needs this to choose the right XEP-0384 wire +// representation (a plain vs a ). +type CiphertextMessage struct { + Type int + Serialized []byte +} + +// SessionCipher performs encrypt/decrypt for an already-established session. +type SessionCipher struct { + raw *C.session_cipher +} + +// NewSessionCipher creates a session cipher for storeCtx's local identity +// talking to remote. version selects the wire format for messages this +// cipher *produces* (ProtocolVersionLegacy or ProtocolVersionModern); +// decrypting adapts to whatever version the incoming message declares. +func NewSessionCipher(ctx *Context, storeCtx *StoreContext, remote Address, version int) (*SessionCipher, error) { + var raw *C.session_cipher + var code C.int + withCAddress(remote, func(cAddr *C.signal_protocol_address) { + code = C.session_cipher_create(&raw, storeCtx.raw, cAddr, ctx.raw) + }) + if code != C.SG_SUCCESS { + return nil, newError("session_cipher_create", int(code)) + } + if err := setCipherVersion(raw, version); err != nil { + C.session_cipher_free(raw) + return nil, err + } + return &SessionCipher{raw: raw}, nil +} + +// Close frees the session cipher. Do not use it afterward. +func (c *SessionCipher) Close() { + if c.raw != nil { + C.session_cipher_free(c.raw) + c.raw = nil + } +} + +// Encrypt encrypts plaintext for this cipher's session, in whichever +// protocol version the cipher was created with. +func (c *SessionCipher) Encrypt(plaintext []byte) (*CiphertextMessage, error) { + var ptr *C.uint8_t + if len(plaintext) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&plaintext[0])) + } + var msg *C.ciphertext_message + code := C.session_cipher_encrypt(c.raw, ptr, C.size_t(len(plaintext)), &msg) + runtime.KeepAlive(plaintext) + if code != C.SG_SUCCESS { + return nil, newError("session_cipher_encrypt", int(code)) + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(msg))) + + return &CiphertextMessage{ + Type: int(C.ciphertext_message_get_type(msg)), + Serialized: bufferToBytes(C.ciphertext_message_get_serialized(msg)), + }, nil +} + +// Decrypt decrypts a message received from this cipher's remote peer. +// +// useOmemoFraming selects between the legacy wire encoding and the OMEMO +// (protocol v4) one for parsing msg.Serialized - it must match whichever +// namespace/version the sender actually used, not necessarily this +// cipher's own outgoing version (a peer's incoming and outgoing wire +// format need not match while a conversation is transitioning between +// legacy and modern OMEMO). ownRegistrationID is this identity's own +// registration id (IdentityStore.GetLocalRegistrationID) - only consulted +// for CiphertextPreKeyType messages under OMEMO framing, which the C API +// requires it for. +func (c *SessionCipher) Decrypt(ctx *Context, msg CiphertextMessage, useOmemoFraming bool, ownRegistrationID uint32) ([]byte, error) { + var ptr *C.uint8_t + if len(msg.Serialized) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&msg.Serialized[0])) + } + + var plaintextBuf *C.signal_buffer + var code C.int + + switch msg.Type { + case CiphertextPreKeyType: + var preKeyMsg *C.pre_key_signal_message + if useOmemoFraming { + preKeyMsg, code = deserializePreKeySignalMessageOmemo(ctx, ptr, C.size_t(len(msg.Serialized)), ownRegistrationID) + } else { + code = C.pre_key_signal_message_deserialize(&preKeyMsg, ptr, C.size_t(len(msg.Serialized)), ctx.raw) + } + if code != C.SG_SUCCESS { + break + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(preKeyMsg))) + code = C.session_cipher_decrypt_pre_key_signal_message(c.raw, preKeyMsg, nil, &plaintextBuf) + case CiphertextSignalType: + var signalMsg *C.signal_message + if useOmemoFraming { + signalMsg, code = deserializeSignalMessageOmemo(ctx, ptr, C.size_t(len(msg.Serialized))) + } else { + code = C.signal_message_deserialize(&signalMsg, ptr, C.size_t(len(msg.Serialized)), ctx.raw) + } + if code != C.SG_SUCCESS { + break + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(signalMsg))) + code = C.session_cipher_decrypt_signal_message(c.raw, signalMsg, nil, &plaintextBuf) + default: + code = C.int(ErrInvalidMessage) + } + runtime.KeepAlive(msg.Serialized) + + if code != C.SG_SUCCESS { + return nil, newError("session_cipher_decrypt", int(code)) + } + defer freeBuffer(plaintextBuf) + return bufferToBytes(plaintextBuf), nil +} diff --git a/e2ee/omemo/libsignal/context.go b/e2ee/omemo/libsignal/context.go new file mode 100644 index 0000000..4cc3d63 --- /dev/null +++ b/e2ee/omemo/libsignal/context.go @@ -0,0 +1,89 @@ +package libsignal + +/* +#include +#include + +// Forward declarations of the Go-exported crypto callbacks implemented in +// crypto.go. Declared here (rather than relying on a generated +// _cgo_export.h) with signatures matching signal_protocol.h's +// signal_crypto_provider field types exactly, so the struct-literal +// assignment below type-checks; the actual symbols are resolved at link +// time regardless of the const-qualifier differences cgo's own generated +// prototypes would have (const has no effect on calling convention). +extern int go_crypto_random(uint8_t *data, size_t len, void *user_data); +extern int go_crypto_hmac_sha256_init(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data); +extern int go_crypto_hmac_sha256_update(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data); +extern int go_crypto_hmac_sha256_final(void *hmac_context, signal_buffer **output, void *user_data); +extern void go_crypto_hmac_sha256_cleanup(void *hmac_context, void *user_data); +extern int go_crypto_sha512_init(void **digest_context, void *user_data); +extern int go_crypto_sha512_update(void *digest_context, const uint8_t *data, size_t data_len, void *user_data); +extern int go_crypto_sha512_final(void *digest_context, signal_buffer **output, void *user_data); +extern void go_crypto_sha512_cleanup(void *digest_context, void *user_data); +extern int go_crypto_encrypt(signal_buffer **output, int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, void *user_data); +extern int go_crypto_decrypt(signal_buffer **output, int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, void *user_data); + +static int telegabber_setup_crypto_provider(signal_context *ctx) { + signal_crypto_provider provider; + provider.random_func = go_crypto_random; + provider.hmac_sha256_init_func = go_crypto_hmac_sha256_init; + provider.hmac_sha256_update_func = go_crypto_hmac_sha256_update; + provider.hmac_sha256_final_func = go_crypto_hmac_sha256_final; + provider.hmac_sha256_cleanup_func = go_crypto_hmac_sha256_cleanup; + provider.sha512_digest_init_func = go_crypto_sha512_init; + provider.sha512_digest_update_func = go_crypto_sha512_update; + provider.sha512_digest_final_func = go_crypto_sha512_final; + provider.sha512_digest_cleanup_func = go_crypto_sha512_cleanup; + provider.encrypt_func = go_crypto_encrypt; + provider.decrypt_func = go_crypto_decrypt; + provider.user_data = 0; + return signal_context_set_crypto_provider(ctx, &provider); +} +*/ +import "C" + +import "errors" + +// Context wraps the global signal_context, wiring up a crypto provider +// backed by Go's standard library (libomemo-c ships no default one). +// +// This binding does not register locking functions with the library (see +// signal_context_set_locking_functions), because the C API requires them to +// support recursive locking, which a plain sync.Mutex cannot safely +// provide. Instead, callers MUST serialize all operations against a given +// Context - and anything built from it (StoreContext, SessionBuilder, +// SessionCipher) - themselves; libomemo-c does not spawn threads of its +// own, so a single mutex held by the caller around each call is sufficient. +type Context struct { + raw *C.signal_context +} + +// NewContext creates and configures a new global library context. +func NewContext() (*Context, error) { + ctx := &Context{} + if code := C.signal_context_create(&ctx.raw, nil); code != C.SG_SUCCESS { + return nil, newError("signal_context_create", int(code)) + } + if code := C.telegabber_setup_crypto_provider(ctx.raw); code != C.SG_SUCCESS { + C.signal_context_destroy(ctx.raw) + return nil, newError("signal_context_set_crypto_provider", int(code)) + } + return ctx, nil +} + +// Close destroys the underlying signal_context. Do not use the Context (or +// anything built from it) afterward. +func (c *Context) Close() { + if c.raw != nil { + C.signal_context_destroy(c.raw) + c.raw = nil + } +} + +var errContextClosed = errors.New("libsignal: context is closed") diff --git a/e2ee/omemo/libsignal/crypto.go b/e2ee/omemo/libsignal/crypto.go new file mode 100644 index 0000000..5ce14a2 --- /dev/null +++ b/e2ee/omemo/libsignal/crypto.go @@ -0,0 +1,205 @@ +package libsignal + +/* +#include +*/ +import "C" + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + rand2 "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "errors" + "hash" + "runtime/cgo" + "unsafe" +) + +// libomemo-c ships no default signal_crypto_provider (confirmed by reading +// signal_protocol.h - only test-only OpenSSL/CommonCrypto implementations +// exist, never installed). This file implements the five callback groups +// documented there - random, HMAC-SHA256, SHA-512, and AES encrypt/decrypt +// in the two modes the library actually asks for (CTR-nopadding and +// CBC-PKCS5; no GCM) - entirely against Go's standard library. +// +// hmac_context/digest_context are opaque per-operation state the library +// holds between init/update/final/cleanup calls. Since C can't safely hold +// a long-lived Go pointer, each is a runtime/cgo.Handle round-tripped +// through a uintptr, exactly like the store callbacks in callbacks.go. + +//export go_crypto_random +func go_crypto_random(data *C.uint8_t, length C.size_t, userData unsafe.Pointer) C.int { + buf := unsafe.Slice((*byte)(unsafe.Pointer(data)), int(length)) + if _, err := rand2.Read(buf); err != nil { + return C.int(ErrUnknown) + } + return C.SG_SUCCESS +} + +type hmacState struct { + h hash.Hash +} + +//export go_crypto_hmac_sha256_init +func go_crypto_hmac_sha256_init(hmacContext *unsafe.Pointer, key *C.uint8_t, keyLen C.size_t, userData unsafe.Pointer) C.int { + keyBytes := cBytesToGo(key, keyLen) + handle := cgo.NewHandle(&hmacState{h: hmac.New(sha256.New, keyBytes)}) + // go vet flags this as "possible misuse of unsafe.Pointer" - expected: a + // cgo.Handle is an opaque uintptr token, never dereferenced as a real + // address, and signal_protocol.h's callback shape requires void* here. + *hmacContext = unsafe.Pointer(uintptr(handle)) + return C.SG_SUCCESS +} + +//export go_crypto_hmac_sha256_update +func go_crypto_hmac_sha256_update(hmacContext unsafe.Pointer, data *C.uint8_t, dataLen C.size_t, userData unsafe.Pointer) C.int { + st, ok := cgo.Handle(uintptr(hmacContext)).Value().(*hmacState) + if !ok { + return C.int(ErrUnknown) + } + st.h.Write(cBytesToGo(data, dataLen)) + return C.SG_SUCCESS +} + +//export go_crypto_hmac_sha256_final +func go_crypto_hmac_sha256_final(hmacContext unsafe.Pointer, output **C.signal_buffer, userData unsafe.Pointer) C.int { + st, ok := cgo.Handle(uintptr(hmacContext)).Value().(*hmacState) + if !ok { + return C.int(ErrUnknown) + } + *output = bytesToBuffer(st.h.Sum(nil)) + st.h.Reset() + return C.SG_SUCCESS +} + +//export go_crypto_hmac_sha256_cleanup +func go_crypto_hmac_sha256_cleanup(hmacContext unsafe.Pointer, userData unsafe.Pointer) { + cgo.Handle(uintptr(hmacContext)).Delete() +} + +type digestState struct { + h hash.Hash +} + +//export go_crypto_sha512_init +func go_crypto_sha512_init(digestContext *unsafe.Pointer, userData unsafe.Pointer) C.int { + handle := cgo.NewHandle(&digestState{h: sha512.New()}) + // See the identical vet note in go_crypto_hmac_sha256_init above. + *digestContext = unsafe.Pointer(uintptr(handle)) + return C.SG_SUCCESS +} + +//export go_crypto_sha512_update +func go_crypto_sha512_update(digestContext unsafe.Pointer, data *C.uint8_t, dataLen C.size_t, userData unsafe.Pointer) C.int { + st, ok := cgo.Handle(uintptr(digestContext)).Value().(*digestState) + if !ok { + return C.int(ErrUnknown) + } + st.h.Write(cBytesToGo(data, dataLen)) + return C.SG_SUCCESS +} + +//export go_crypto_sha512_final +func go_crypto_sha512_final(digestContext unsafe.Pointer, output **C.signal_buffer, userData unsafe.Pointer) C.int { + st, ok := cgo.Handle(uintptr(digestContext)).Value().(*digestState) + if !ok { + return C.int(ErrUnknown) + } + *output = bytesToBuffer(st.h.Sum(nil)) + st.h.Reset() + return C.SG_SUCCESS +} + +//export go_crypto_sha512_cleanup +func go_crypto_sha512_cleanup(digestContext unsafe.Pointer, userData unsafe.Pointer) { + cgo.Handle(uintptr(digestContext)).Delete() +} + +//export go_crypto_encrypt +func go_crypto_encrypt(output **C.signal_buffer, cipherMode C.int, key *C.uint8_t, keyLen C.size_t, iv *C.uint8_t, ivLen C.size_t, plaintext *C.uint8_t, plaintextLen C.size_t, userData unsafe.Pointer) C.int { + block, err := aes.NewCipher(cBytesToGo(key, keyLen)) + if err != nil { + return C.int(ErrUnknown) + } + ivBytes := cBytesToGo(iv, ivLen) + plaintextBytes := cBytesToGo(plaintext, plaintextLen) + + var out []byte + switch cipherMode { + case C.SG_CIPHER_AES_CTR_NOPADDING: + out = make([]byte, len(plaintextBytes)) + cipher.NewCTR(block, ivBytes).XORKeyStream(out, plaintextBytes) + case C.SG_CIPHER_AES_CBC_PKCS5: + padded := pkcs7Pad(plaintextBytes, aes.BlockSize) + out = make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, ivBytes).CryptBlocks(out, padded) + default: + return C.int(ErrInvalidArgument) + } + + *output = bytesToBuffer(out) + return C.SG_SUCCESS +} + +//export go_crypto_decrypt +func go_crypto_decrypt(output **C.signal_buffer, cipherMode C.int, key *C.uint8_t, keyLen C.size_t, iv *C.uint8_t, ivLen C.size_t, ciphertext *C.uint8_t, ciphertextLen C.size_t, userData unsafe.Pointer) C.int { + block, err := aes.NewCipher(cBytesToGo(key, keyLen)) + if err != nil { + return C.int(ErrUnknown) + } + ivBytes := cBytesToGo(iv, ivLen) + ciphertextBytes := cBytesToGo(ciphertext, ciphertextLen) + + var out []byte + switch cipherMode { + case C.SG_CIPHER_AES_CTR_NOPADDING: + out = make([]byte, len(ciphertextBytes)) + cipher.NewCTR(block, ivBytes).XORKeyStream(out, ciphertextBytes) + case C.SG_CIPHER_AES_CBC_PKCS5: + if len(ciphertextBytes) == 0 || len(ciphertextBytes)%aes.BlockSize != 0 { + return C.int(ErrInvalidMessage) + } + decrypted := make([]byte, len(ciphertextBytes)) + cipher.NewCBCDecrypter(block, ivBytes).CryptBlocks(decrypted, ciphertextBytes) + unpadded, err := pkcs7Unpad(decrypted) + if err != nil { + return C.int(ErrInvalidMessage) + } + out = unpadded + default: + return C.int(ErrInvalidArgument) + } + + *output = bytesToBuffer(out) + return C.SG_SUCCESS +} + +func pkcs7Pad(data []byte, blockSize int) []byte { + padLen := blockSize - (len(data) % blockSize) + padded := make([]byte, len(data)+padLen) + copy(padded, data) + for i := len(data); i < len(padded); i++ { + padded[i] = byte(padLen) + } + return padded +} + +func pkcs7Unpad(data []byte) ([]byte, error) { + n := len(data) + if n == 0 { + return nil, errors.New("libsignal: cannot unpad empty data") + } + padLen := int(data[n-1]) + if padLen == 0 || padLen > n || padLen > aes.BlockSize { + return nil, errors.New("libsignal: invalid PKCS7 padding") + } + for _, b := range data[n-padLen:] { + if int(b) != padLen { + return nil, errors.New("libsignal: invalid PKCS7 padding") + } + } + return data[:n-padLen], nil +} diff --git a/e2ee/omemo/libsignal/deserialize_legacy.go b/e2ee/omemo/libsignal/deserialize_legacy.go new file mode 100644 index 0000000..e9ad47e --- /dev/null +++ b/e2ee/omemo/libsignal/deserialize_legacy.go @@ -0,0 +1,23 @@ +//go:build signal_legacy + +package libsignal + +/* +#include +#include +*/ +import "C" + +// Vanilla libsignal-protocol-c has no OMEMO (protocol v4) wire format +// support at all, so these always fail with ErrInvalidVersion - callers +// (SessionCipher.Decrypt) should never reach here with useOmemoFraming set +// under this build, since ModernOMEMOSupported is false and the caller is +// expected to check it first. + +func deserializeSignalMessageOmemo(ctx *Context, data *C.uint8_t, length C.size_t) (*C.signal_message, C.int) { + return nil, C.int(ErrInvalidVersion) +} + +func deserializePreKeySignalMessageOmemo(ctx *Context, data *C.uint8_t, length C.size_t, registrationID uint32) (*C.pre_key_signal_message, C.int) { + return nil, C.int(ErrInvalidVersion) +} diff --git a/e2ee/omemo/libsignal/deserialize_modern.go b/e2ee/omemo/libsignal/deserialize_modern.go new file mode 100644 index 0000000..6732b07 --- /dev/null +++ b/e2ee/omemo/libsignal/deserialize_modern.go @@ -0,0 +1,27 @@ +//go:build !signal_legacy + +package libsignal + +/* +#include +#include +*/ +import "C" + +// deserializeSignalMessageOmemo and deserializePreKeySignalMessageOmemo +// parse the OMEMO (protocol v4) wire encoding, which only libomemo-c +// understands - signal_message_deserialize_omemo and +// pre_key_signal_message_deserialize_omemo don't exist in vanilla +// libsignal-protocol-c (see deserialize_legacy.go). + +func deserializeSignalMessageOmemo(ctx *Context, data *C.uint8_t, length C.size_t) (*C.signal_message, C.int) { + var msg *C.signal_message + code := C.signal_message_deserialize_omemo(&msg, data, length, ctx.raw) + return msg, code +} + +func deserializePreKeySignalMessageOmemo(ctx *Context, data *C.uint8_t, length C.size_t, registrationID uint32) (*C.pre_key_signal_message, C.int) { + var msg *C.pre_key_signal_message + code := C.pre_key_signal_message_deserialize_omemo(&msg, data, length, C.uint32_t(registrationID), ctx.raw) + return msg, code +} diff --git a/e2ee/omemo/libsignal/doc.go b/e2ee/omemo/libsignal/doc.go new file mode 100644 index 0000000..7041837 --- /dev/null +++ b/e2ee/omemo/libsignal/doc.go @@ -0,0 +1,29 @@ +// Package libsignal is a cgo binding around libomemo-c (github.com/dino/libomemo-c), +// a fork of libsignal-protocol-c that implements both legacy "siacs" OMEMO +// (protocol version 3) and modern OMEMO 1/OMEMO 2 (protocol version 4) on a +// shared Double Ratchet / X3DH crypto core. +// +// This package deliberately knows nothing about XMPP or OMEMO's XML wire +// format (device lists, bundles, the stanza shape) - it only +// exposes the underlying session/cipher primitives and a pluggable Store +// interface for persistence. The XEP-0384 specific layer lives above this +// package, in dev.narayana.im/narayana/telegabber/e2ee/omemo. +// +// # Build modes +// +// By default this package links libomemo-c and supports both legacy and +// modern OMEMO. Passing the "signal_legacy" build tag (e.g. `go build +// -tags signal_legacy`) instead links the pre-fork libsignal-protocol-c - +// useful on distros that only package that one (e.g. Debian bullseye ships +// libsignal-protocol-c-dev but not libomemo-c-dev, which arrived in +// bookworm). That build only supports legacy/siacs OMEMO; check +// ModernOMEMOSupported before relying on anything else. See cgo_modern.go/ +// cgo_legacy.go and the version_*/deserialize_*/signedprekey_* files for +// the resulting API-surface differences between the two libraries. +// +// Concurrency: neither library is internally thread-safe (this binding +// does not wire up the optional locking-function hook - see Context). +// Callers must serialize all operations against a given Context/ +// StoreContext pair themselves, e.g. with a single mutex per bridged chat +// identity. +package libsignal diff --git a/e2ee/omemo/libsignal/errors.go b/e2ee/omemo/libsignal/errors.go new file mode 100644 index 0000000..ce1fd24 --- /dev/null +++ b/e2ee/omemo/libsignal/errors.go @@ -0,0 +1,113 @@ +package libsignal + +import "fmt" + +// Error codes as defined in signal_protocol.h. Duplicated here as plain Go +// constants (rather than referenced via cgo) so this file has no cgo +// dependency and callers outside this package can compare against them. +const ( + ErrNoMemory = -12 + ErrInvalidArgument = -22 + ErrUnknown = -1000 + ErrDuplicateMessage = -1001 + ErrInvalidKey = -1002 + ErrInvalidKeyID = -1003 + ErrInvalidMAC = -1004 + ErrInvalidMessage = -1005 + ErrInvalidVersion = -1006 + ErrLegacyMessage = -1007 + ErrNoSession = -1008 + ErrStaleKeyExchange = -1009 + ErrUntrustedIdentity = -1010 + ErrVRFSigVerifyFailed = -1011 + ErrInvalidProtobuf = -1100 + ErrFPVersionMismatch = -1200 + ErrFPIdentMismatch = -1201 +) + +// Error wraps a libsignal-c return code. +type Error struct { + Code int + Op string +} + +func (e *Error) Error() string { + return fmt.Sprintf("libsignal: %s: %s (%d)", e.Op, errName(e.Code), e.Code) +} + +func errName(code int) string { + switch code { + case ErrNoMemory: + return "out of memory" + case ErrInvalidArgument: + return "invalid argument" + case ErrUnknown: + return "unknown error" + case ErrDuplicateMessage: + return "duplicate message" + case ErrInvalidKey: + return "invalid key" + case ErrInvalidKeyID: + return "invalid key id" + case ErrInvalidMAC: + return "invalid MAC" + case ErrInvalidMessage: + return "invalid message" + case ErrInvalidVersion: + return "invalid version" + case ErrLegacyMessage: + return "message uses a protocol version no longer supported" + case ErrNoSession: + return "no session" + case ErrStaleKeyExchange: + return "stale key exchange" + case ErrUntrustedIdentity: + return "untrusted identity" + case ErrVRFSigVerifyFailed: + return "VRF signature verification failed" + case ErrInvalidProtobuf: + return "invalid protobuf" + case ErrFPVersionMismatch: + return "fingerprint version mismatch" + case ErrFPIdentMismatch: + return "fingerprint identity mismatch" + default: + return "library error" + } +} + +// newError returns nil for a non-negative (success) return code, or an +// *Error wrapping it otherwise. op names the operation that produced code, +// for diagnostics. +func newError(op string, code int) error { + if code >= 0 { + return nil + } + return &Error{Code: code, Op: op} +} + +// IsUntrustedIdentity reports whether err is (or wraps) SG_ERR_UNTRUSTED_IDENTITY - +// the signal that a remote device's identity key changed since it was last +// trusted (the case a TOFU trust policy must not silently paper over). +func IsUntrustedIdentity(err error) bool { + e, ok := err.(*Error) + return ok && e.Code == ErrUntrustedIdentity +} + +// IsNoSession reports whether err is (or wraps) SG_ERR_NO_SESSION. +func IsNoSession(err error) bool { + e, ok := err.(*Error) + return ok && e.Code == ErrNoSession +} + +// IsDuplicateMessage reports whether err is (or wraps) SG_ERR_DUPLICATE_MESSAGE. +func IsDuplicateMessage(err error) bool { + e, ok := err.(*Error) + return ok && e.Code == ErrDuplicateMessage +} + +// IsLegacyMessage reports whether err is (or wraps) SG_ERR_LEGACY_MESSAGE. +func IsLegacyMessage(err error) bool { + e, ok := err.(*Error) + return ok && e.Code == ErrLegacyMessage +} diff --git a/e2ee/omemo/libsignal/keys.go b/e2ee/omemo/libsignal/keys.go new file mode 100644 index 0000000..7ad4ae3 --- /dev/null +++ b/e2ee/omemo/libsignal/keys.go @@ -0,0 +1,268 @@ +package libsignal + +/* +#include +#include +#include +#include +#include +*/ +import "C" + +import ( + "runtime" + "time" + "unsafe" +) + +// IdentityKeyPair is the serialized (protobuf) form of a Curve25519 +// identity key pair, as produced by ratchet_identity_key_pair_serialize. +// This is the form to persist; use SplitIdentityKeyPair/DecodeIdentityPublicKey +// to recover the individual keys when needed. +type IdentityKeyPair struct { + Record []byte +} + +// GenerateIdentityKeyPair creates a new identity key pair. Do this once per +// bridged-chat identity, at first use, and persist the result. +func GenerateIdentityKeyPair(ctx *Context) (*IdentityKeyPair, error) { + var kp *C.ratchet_identity_key_pair + if code := C.signal_protocol_key_helper_generate_identity_key_pair(&kp, ctx.raw); code != C.SG_SUCCESS { + return nil, newError("signal_protocol_key_helper_generate_identity_key_pair", int(code)) + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(kp))) + + var buf *C.signal_buffer + if code := C.ratchet_identity_key_pair_serialize(&buf, kp); code != C.SG_SUCCESS { + return nil, newError("ratchet_identity_key_pair_serialize", int(code)) + } + defer freeBuffer(buf) + + return &IdentityKeyPair{Record: bufferToBytes(buf)}, nil +} + +func deserializeIdentityKeyPair(ctx *Context, identity *IdentityKeyPair) (*C.ratchet_identity_key_pair, error) { + var kp *C.ratchet_identity_key_pair + data := identity.Record + var ptr *C.uint8_t + if len(data) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&data[0])) + } + code := C.ratchet_identity_key_pair_deserialize(&kp, ptr, C.size_t(len(data)), ctx.raw) + runtime.KeepAlive(data) + if code != C.SG_SUCCESS { + return nil, newError("ratchet_identity_key_pair_deserialize", int(code)) + } + return kp, nil +} + +// SplitIdentityKeyPair deserializes a combined identity key pair record (as +// produced by GenerateIdentityKeyPair) into the separate public/private key +// buffers the library's identity-key-store callback (IdentityStore. +// GetIdentityKeyPair) is expected to return. +func SplitIdentityKeyPair(ctx *Context, record []byte) (public, private []byte, err error) { + kp, err := deserializeIdentityKeyPair(ctx, &IdentityKeyPair{Record: record}) + if err != nil { + return nil, nil, err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(kp))) + + var pubBuf *C.signal_buffer + if code := C.ec_public_key_serialize(&pubBuf, C.ratchet_identity_key_pair_get_public(kp)); code != C.SG_SUCCESS { + return nil, nil, newError("ec_public_key_serialize", int(code)) + } + defer freeBuffer(pubBuf) + + var privBuf *C.signal_buffer + if code := C.ec_private_key_serialize(&privBuf, C.ratchet_identity_key_pair_get_private(kp)); code != C.SG_SUCCESS { + return nil, nil, newError("ec_private_key_serialize", int(code)) + } + defer freeBuffer(privBuf) + + return bufferToBytes(pubBuf), bufferToBytes(privBuf), nil +} + +// DecodeIdentityPublicKey extracts the raw public key bytes from a combined +// identity key pair record, for publishing in an XEP-0384 bundle. +func DecodeIdentityPublicKey(ctx *Context, record []byte) ([]byte, error) { + kp, err := deserializeIdentityKeyPair(ctx, &IdentityKeyPair{Record: record}) + if err != nil { + return nil, err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(kp))) + + var pubBuf *C.signal_buffer + if code := C.ec_public_key_serialize(&pubBuf, C.ratchet_identity_key_pair_get_public(kp)); code != C.SG_SUCCESS { + return nil, newError("ec_public_key_serialize", int(code)) + } + defer freeBuffer(pubBuf) + return bufferToBytes(pubBuf), nil +} + +// GenerateRegistrationID creates a new registration id - a random number +// each identity picks once, at "install" time. +func GenerateRegistrationID(ctx *Context) (uint32, error) { + var id C.uint32_t + if code := C.signal_protocol_key_helper_generate_registration_id(&id, 0, ctx.raw); code != C.SG_SUCCESS { + return 0, newError("signal_protocol_key_helper_generate_registration_id", int(code)) + } + return uint32(id), nil +} + +// PreKey is a serialized one-time prekey record plus its numeric id. +type PreKey struct { + ID uint32 + Record []byte +} + +// GeneratePreKeys creates count one-time prekeys with sequential ids +// starting at start. Store every one; each is consumed (and should then be +// deleted) the first time a remote peer's session is built from it. +func GeneratePreKeys(ctx *Context, start, count uint32) ([]PreKey, error) { + var head *C.signal_protocol_key_helper_pre_key_list_node + if code := C.signal_protocol_key_helper_generate_pre_keys(&head, C.uint(start), C.uint(count), ctx.raw); code != C.SG_SUCCESS { + return nil, newError("signal_protocol_key_helper_generate_pre_keys", int(code)) + } + defer C.signal_protocol_key_helper_key_list_free(head) + + var preKeys []PreKey + for node := head; node != nil; node = C.signal_protocol_key_helper_key_list_next(node) { + pk := C.signal_protocol_key_helper_key_list_element(node) + var buf *C.signal_buffer + if code := C.session_pre_key_serialize(&buf, pk); code != C.SG_SUCCESS { + return nil, newError("session_pre_key_serialize", int(code)) + } + preKeys = append(preKeys, PreKey{ + ID: uint32(C.session_pre_key_get_id(pk)), + Record: bufferToBytes(buf), + }) + freeBuffer(buf) + } + return preKeys, nil +} + +func deserializePreKey(ctx *Context, record []byte) (*C.session_pre_key, error) { + var pk *C.session_pre_key + var ptr *C.uint8_t + if len(record) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&record[0])) + } + code := C.session_pre_key_deserialize(&pk, ptr, C.size_t(len(record)), ctx.raw) + runtime.KeepAlive(record) + if code != C.SG_SUCCESS { + return nil, newError("session_pre_key_deserialize", int(code)) + } + return pk, nil +} + +// PreKeyInfo is the decoded, ready-to-publish material for a one-time prekey. +type PreKeyInfo struct { + ID uint32 + PublicKey []byte +} + +// DecodePreKey deserializes a stored one-time prekey record (as produced by +// GeneratePreKeys) and extracts its id and raw public key, for publishing in +// an XEP-0384 bundle. +func DecodePreKey(ctx *Context, record []byte) (*PreKeyInfo, error) { + pk, err := deserializePreKey(ctx, record) + if err != nil { + return nil, err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(pk))) + + var pubBuf *C.signal_buffer + if code := C.ec_public_key_serialize(&pubBuf, C.ec_key_pair_get_public(C.session_pre_key_get_key_pair(pk))); code != C.SG_SUCCESS { + return nil, newError("ec_public_key_serialize", int(code)) + } + defer freeBuffer(pubBuf) + + return &PreKeyInfo{ + ID: uint32(C.session_pre_key_get_id(pk)), + PublicKey: bufferToBytes(pubBuf), + }, nil +} + +// SignedPreKey is a serialized signed prekey record plus its numeric id. +type SignedPreKey struct { + ID uint32 + Record []byte +} + +// GenerateSignedPreKey creates a new signed prekey, signed by identity (in +// its combined serialized form, as produced by GenerateIdentityKeyPair), +// with the given id. +func GenerateSignedPreKey(ctx *Context, identity *IdentityKeyPair, id uint32) (*SignedPreKey, error) { + idKeyPair, err := deserializeIdentityKeyPair(ctx, identity) + if err != nil { + return nil, err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(idKeyPair))) + + var signedPreKey *C.session_signed_pre_key + timestamp := C.uint64_t(time.Now().UnixMilli()) + if code := C.signal_protocol_key_helper_generate_signed_pre_key(&signedPreKey, idKeyPair, C.uint32_t(id), timestamp, ctx.raw); code != C.SG_SUCCESS { + return nil, newError("signal_protocol_key_helper_generate_signed_pre_key", int(code)) + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(signedPreKey))) + + var buf *C.signal_buffer + if code := C.session_signed_pre_key_serialize(&buf, signedPreKey); code != C.SG_SUCCESS { + return nil, newError("session_signed_pre_key_serialize", int(code)) + } + defer freeBuffer(buf) + + return &SignedPreKey{ + ID: uint32(C.session_signed_pre_key_get_id(signedPreKey)), + Record: bufferToBytes(buf), + }, nil +} + +func deserializeSignedPreKey(ctx *Context, record []byte) (*C.session_signed_pre_key, error) { + var spk *C.session_signed_pre_key + var ptr *C.uint8_t + if len(record) > 0 { + ptr = (*C.uint8_t)(unsafe.Pointer(&record[0])) + } + code := C.session_signed_pre_key_deserialize(&spk, ptr, C.size_t(len(record)), ctx.raw) + runtime.KeepAlive(record) + if code != C.SG_SUCCESS { + return nil, newError("session_signed_pre_key_deserialize", int(code)) + } + return spk, nil +} + +// SignedPreKeyInfo is the decoded, ready-to-publish material for a signed +// prekey: its numeric id, raw public key, and (both) signature forms the +// library maintains - a legacy signature and an OMEMO signature (libomemo-c +// computes both regardless of which protocol version ends up using it). +type SignedPreKeyInfo struct { + ID uint32 + PublicKey []byte + Signature []byte + SignatureOMEMO []byte +} + +// DecodeSignedPreKey deserializes a stored signed prekey record (as produced +// by GenerateSignedPreKey) and extracts the fields needed to publish it in +// an XEP-0384 bundle. +func DecodeSignedPreKey(ctx *Context, record []byte) (*SignedPreKeyInfo, error) { + spk, err := deserializeSignedPreKey(ctx, record) + if err != nil { + return nil, err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(spk))) + + var pubBuf *C.signal_buffer + if code := C.ec_public_key_serialize(&pubBuf, C.ec_key_pair_get_public(C.session_signed_pre_key_get_key_pair(spk))); code != C.SG_SUCCESS { + return nil, newError("ec_public_key_serialize", int(code)) + } + defer freeBuffer(pubBuf) + + return &SignedPreKeyInfo{ + ID: uint32(C.session_signed_pre_key_get_id(spk)), + PublicKey: bufferToBytes(pubBuf), + Signature: cBytesToGo(C.session_signed_pre_key_get_signature(spk), C.session_signed_pre_key_get_signature_len(spk)), + SignatureOMEMO: signedPreKeyOmemoSignature(spk), + }, nil +} diff --git a/e2ee/omemo/libsignal/session.go b/e2ee/omemo/libsignal/session.go new file mode 100644 index 0000000..ed4d99d --- /dev/null +++ b/e2ee/omemo/libsignal/session.go @@ -0,0 +1,142 @@ +package libsignal + +/* +#include +#include +#include +#include +*/ +import "C" + +import ( + "runtime" + "unsafe" +) + +// Protocol version constants (protocol.h's CIPHERTEXT_CURRENT_VERSION / +// CIPHERTEXT_OMEMO_VERSION), selecting legacy siacs OMEMO vs modern OMEMO. +// Modern is shared by OMEMO 1 (urn:xmpp:omemo:1) and OMEMO 2 +// (urn:xmpp:omemo:2) - those two differ only in outer SCE stanza framing, +// not in this underlying Double Ratchet/X3DH wire format. +const ( + ProtocolVersionLegacy = 3 + ProtocolVersionModern = 4 +) + +// RemoteBundle is the decoded material from a peer's XEP-0384 device +// bundle, as needed to establish a session with one of their devices via +// X3DH. +type RemoteBundle struct { + RegistrationID uint32 + DeviceID uint32 + PreKeyID uint32 // 0 if the bundle carried no one-time prekey + PreKeyPublic []byte // nil if PreKeyID == 0 + SignedPreKeyID uint32 + SignedPreKeyPublic []byte + SignedPreKeySignature []byte + IdentityKeyPublic []byte +} + +// SessionBuilder establishes (or refreshes) a Double Ratchet session with +// one remote device. +type SessionBuilder struct { + raw *C.session_builder +} + +// NewSessionBuilder creates a session builder for storeCtx's local identity +// talking to remote. version controls the protocol version new sessions +// built by ProcessPreKeyBundle will use for encryption going forward +// (ProtocolVersionLegacy or ProtocolVersionModern). +func NewSessionBuilder(ctx *Context, storeCtx *StoreContext, remote Address, version int) (*SessionBuilder, error) { + var raw *C.session_builder + var code C.int + withCAddress(remote, func(cAddr *C.signal_protocol_address) { + code = C.session_builder_create(&raw, storeCtx.raw, cAddr, ctx.raw) + }) + if code != C.SG_SUCCESS { + return nil, newError("session_builder_create", int(code)) + } + if err := setBuilderVersion(raw, version); err != nil { + C.session_builder_free(raw) + return nil, err + } + return &SessionBuilder{raw: raw}, nil +} + +// Close frees the session builder. Do not use it afterward. +func (b *SessionBuilder) Close() { + if b.raw != nil { + C.session_builder_free(b.raw) + b.raw = nil + } +} + +// ProcessPreKeyBundle establishes a session from a remote peer's fetched +// device bundle (X3DH). +// +// IsUntrustedIdentity(err) reports the case where the remote's identity key +// does not match a previously trusted one for this device id - under a +// TOFU trust policy this is the one case that must NOT be silently +// re-trusted (it is what TOFU exists to catch: either genuine key rotation +// or an impersonation attempt), unlike a first-ever sighting of a device, +// which the identity store's IsTrustedIdentity should accept. +func (b *SessionBuilder) ProcessPreKeyBundle(ctx *Context, bundle RemoteBundle) error { + identityKey, err := decodePoint(ctx, bundle.IdentityKeyPublic) + if err != nil { + return err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(identityKey))) + + signedPreKeyPublic, err := decodePoint(ctx, bundle.SignedPreKeyPublic) + if err != nil { + return err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(signedPreKeyPublic))) + + var preKeyPublic *C.ec_public_key + if len(bundle.PreKeyPublic) > 0 { + preKeyPublic, err = decodePoint(ctx, bundle.PreKeyPublic) + if err != nil { + return err + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(preKeyPublic))) + } + + sigPtr, sigLen := cBytesPtrLen(bundle.SignedPreKeySignature) + + var cBundle *C.session_pre_key_bundle + code := C.session_pre_key_bundle_create(&cBundle, + C.uint32_t(bundle.RegistrationID), C.int(bundle.DeviceID), + C.uint32_t(bundle.PreKeyID), preKeyPublic, + C.uint32_t(bundle.SignedPreKeyID), signedPreKeyPublic, + sigPtr, sigLen, + identityKey) + runtime.KeepAlive(bundle.SignedPreKeySignature) + if code != C.SG_SUCCESS { + return newError("session_pre_key_bundle_create", int(code)) + } + defer C.signal_type_unref((*C.signal_type_base)(unsafe.Pointer(cBundle))) + + if code := C.session_builder_process_pre_key_bundle(b.raw, cBundle); code != C.SG_SUCCESS { + return newError("session_builder_process_pre_key_bundle", int(code)) + } + return nil +} + +func decodePoint(ctx *Context, data []byte) (*C.ec_public_key, error) { + var key *C.ec_public_key + ptr, length := cBytesPtrLen(data) + code := C.curve_decode_point(&key, ptr, length, ctx.raw) + runtime.KeepAlive(data) + if code != C.SG_SUCCESS { + return nil, newError("curve_decode_point", int(code)) + } + return key, nil +} + +func cBytesPtrLen(b []byte) (*C.uint8_t, C.size_t) { + if len(b) == 0 { + return nil, 0 + } + return (*C.uint8_t)(unsafe.Pointer(&b[0])), C.size_t(len(b)) +} diff --git a/e2ee/omemo/libsignal/signedprekey_legacy.go b/e2ee/omemo/libsignal/signedprekey_legacy.go new file mode 100644 index 0000000..bc1897e --- /dev/null +++ b/e2ee/omemo/libsignal/signedprekey_legacy.go @@ -0,0 +1,15 @@ +//go:build signal_legacy + +package libsignal + +/* +#include +#include +*/ +import "C" + +// Vanilla libsignal-protocol-c's signed prekeys carry only the one +// (legacy) signature - there is no second OMEMO signature to extract. +func signedPreKeyOmemoSignature(spk *C.session_signed_pre_key) []byte { + return nil +} diff --git a/e2ee/omemo/libsignal/signedprekey_modern.go b/e2ee/omemo/libsignal/signedprekey_modern.go new file mode 100644 index 0000000..184b662 --- /dev/null +++ b/e2ee/omemo/libsignal/signedprekey_modern.go @@ -0,0 +1,17 @@ +//go:build !signal_legacy + +package libsignal + +/* +#include +#include +*/ +import "C" + +// signedPreKeyOmemoSignature extracts the OMEMO (XEdDSA/Ed25519) signature +// libomemo-c computes on every signed prekey alongside the legacy one - +// vanilla libsignal-protocol-c has no such second signature (see +// signedprekey_legacy.go). +func signedPreKeyOmemoSignature(spk *C.session_signed_pre_key) []byte { + return cBytesToGo(C.session_signed_pre_key_get_signature_omemo(spk), C.session_signed_pre_key_get_signature_omemo_len(spk)) +} diff --git a/e2ee/omemo/libsignal/store.go b/e2ee/omemo/libsignal/store.go new file mode 100644 index 0000000..f2eae49 --- /dev/null +++ b/e2ee/omemo/libsignal/store.go @@ -0,0 +1,204 @@ +package libsignal + +/* +#include +#include + +// Forward declarations of the Go-exported store callbacks implemented in +// callbacks.go. See context.go for why these are hand-written rather than +// relying on a generated header, and why const-qualifier differences from +// the "true" cgo-generated prototypes are harmless here. +extern int go_get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data); +extern int go_get_local_registration_id(void *user_data, uint32_t *registration_id); +extern int go_save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); +extern int go_is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); +extern void go_identity_destroy(void *user_data); + +extern int go_load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data); +extern int go_store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data); +extern int go_contains_pre_key(uint32_t pre_key_id, void *user_data); +extern int go_remove_pre_key(uint32_t pre_key_id, void *user_data); +extern void go_pre_key_destroy(void *user_data); + +extern int go_load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data); +extern int go_store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data); +extern int go_contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); +extern int go_remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); +extern void go_signed_pre_key_destroy(void *user_data); + +extern int go_load_session(signal_buffer **record, signal_buffer **user_record, const signal_protocol_address *address, void *user_data); +extern int go_get_sub_device_sessions(signal_int_list **sessions, const char *name, size_t name_len, void *user_data); +extern int go_store_session(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data); +extern int go_contains_session(const signal_protocol_address *address, void *user_data); +extern int go_delete_session(const signal_protocol_address *address, void *user_data); +extern int go_delete_all_sessions(const char *name, size_t name_len, void *user_data); +extern void go_session_destroy(void *user_data); + +static void telegabber_fill_identity_key_store(signal_protocol_identity_key_store *s, void *user_data) { + s->get_identity_key_pair = go_get_identity_key_pair; + s->get_local_registration_id = go_get_local_registration_id; + s->save_identity = go_save_identity; + s->is_trusted_identity = go_is_trusted_identity; + s->destroy_func = go_identity_destroy; + s->user_data = user_data; +} + +static void telegabber_fill_pre_key_store(signal_protocol_pre_key_store *s, void *user_data) { + s->load_pre_key = go_load_pre_key; + s->store_pre_key = go_store_pre_key; + s->contains_pre_key = go_contains_pre_key; + s->remove_pre_key = go_remove_pre_key; + s->destroy_func = go_pre_key_destroy; + s->user_data = user_data; +} + +static void telegabber_fill_signed_pre_key_store(signal_protocol_signed_pre_key_store *s, void *user_data) { + s->load_signed_pre_key = go_load_signed_pre_key; + s->store_signed_pre_key = go_store_signed_pre_key; + s->contains_signed_pre_key = go_contains_signed_pre_key; + s->remove_signed_pre_key = go_remove_signed_pre_key; + s->destroy_func = go_signed_pre_key_destroy; + s->user_data = user_data; +} + +static void telegabber_fill_session_store(signal_protocol_session_store *s, void *user_data) { + s->load_session_func = go_load_session; + s->get_sub_device_sessions_func = go_get_sub_device_sessions; + s->store_session_func = go_store_session; + s->contains_session_func = go_contains_session; + s->delete_session_func = go_delete_session; + s->delete_all_sessions_func = go_delete_all_sessions; + s->destroy_func = go_session_destroy; + s->user_data = user_data; +} +*/ +import "C" + +import ( + "runtime/cgo" + "unsafe" +) + +// IdentityStore persists the local identity key pair/registration id and +// tracks trust for remote identities. +type IdentityStore interface { + // GetIdentityKeyPair returns this store's own identity key pair + // (public and private key, in the library's serialized form). + GetIdentityKeyPair() (public, private []byte, err error) + // GetLocalRegistrationID returns this store's own registration id. + GetLocalRegistrationID() (uint32, error) + // SaveIdentity records addr's identity key as trusted. A nil key + // means: forget the key material but keep any other bookkeeping. + SaveIdentity(addr Address, key []byte) error + // IsTrustedIdentity reports whether key is the trusted identity key + // for addr. Per the library's TOFU convention, an address with no + // stored identity at all is trusted implicitly (return true, nil) - + // only a *mismatching* stored key should be rejected. + IsTrustedIdentity(addr Address, key []byte) (bool, error) +} + +// PreKeyStore persists this identity's own one-time prekeys. +type PreKeyStore interface { + LoadPreKey(id uint32) (record []byte, found bool, err error) + StorePreKey(id uint32, record []byte) error + ContainsPreKey(id uint32) bool + RemovePreKey(id uint32) error +} + +// SignedPreKeyStore persists this identity's own signed prekeys. +type SignedPreKeyStore interface { + LoadSignedPreKey(id uint32) (record []byte, found bool, err error) + StoreSignedPreKey(id uint32, record []byte) error + ContainsSignedPreKey(id uint32) bool + RemoveSignedPreKey(id uint32) error +} + +// SessionStore persists per-device Double Ratchet session state. +type SessionStore interface { + LoadSession(addr Address) (record []byte, found bool, err error) + // GetSubDeviceSessions returns the device IDs of all known sessions + // for name, excluding the sentinel device ID 1 (per library + // convention - see get_sub_device_sessions_func in signal_protocol.h). + GetSubDeviceSessions(name string) ([]uint32, error) + StoreSession(addr Address, record []byte) error + ContainsSession(addr Address) bool + DeleteSession(addr Address) error + // DeleteAllSessions removes every session for name and returns how + // many were deleted. + DeleteAllSessions(name string) (int, error) +} + +// Store is the full persistence contract libomemo-c requires: one local +// identity's keys, prekeys, and remote sessions/trust state. +type Store interface { + IdentityStore + PreKeyStore + SignedPreKeyStore + SessionStore +} + +// StoreContext wraps a signal_protocol_store_context bound to a Go Store +// implementation via a runtime/cgo.Handle (C void* cannot safely hold a Go +// interface value directly - see callbacks.go for the recovery side). +type StoreContext struct { + raw *C.signal_protocol_store_context + ctx *Context + handle cgo.Handle +} + +// NewStoreContext creates a signal_protocol_store_context that dispatches +// every callback to store. ctx must outlive the returned StoreContext. +func NewStoreContext(ctx *Context, store Store) (*StoreContext, error) { + var raw *C.signal_protocol_store_context + if code := C.signal_protocol_store_context_create(&raw, ctx.raw); code != C.SG_SUCCESS { + return nil, newError("signal_protocol_store_context_create", int(code)) + } + + handle := cgo.NewHandle(store) + // go vet flags this as "possible misuse of unsafe.Pointer" - expected, + // see the note in crypto.go's go_crypto_hmac_sha256_init: a cgo.Handle + // is an opaque uintptr token, never dereferenced as a real address, and + // the vtable structs' user_data fields require void* here. + userData := unsafe.Pointer(uintptr(handle)) + + var idStore C.signal_protocol_identity_key_store + C.telegabber_fill_identity_key_store(&idStore, userData) + if code := C.signal_protocol_store_context_set_identity_key_store(raw, &idStore); code != C.SG_SUCCESS { + handle.Delete() + return nil, newError("signal_protocol_store_context_set_identity_key_store", int(code)) + } + + var pkStore C.signal_protocol_pre_key_store + C.telegabber_fill_pre_key_store(&pkStore, userData) + if code := C.signal_protocol_store_context_set_pre_key_store(raw, &pkStore); code != C.SG_SUCCESS { + handle.Delete() + return nil, newError("signal_protocol_store_context_set_pre_key_store", int(code)) + } + + var spkStore C.signal_protocol_signed_pre_key_store + C.telegabber_fill_signed_pre_key_store(&spkStore, userData) + if code := C.signal_protocol_store_context_set_signed_pre_key_store(raw, &spkStore); code != C.SG_SUCCESS { + handle.Delete() + return nil, newError("signal_protocol_store_context_set_signed_pre_key_store", int(code)) + } + + var sessStore C.signal_protocol_session_store + C.telegabber_fill_session_store(&sessStore, userData) + if code := C.signal_protocol_store_context_set_session_store(raw, &sessStore); code != C.SG_SUCCESS { + handle.Delete() + return nil, newError("signal_protocol_store_context_set_session_store", int(code)) + } + + return &StoreContext{raw: raw, ctx: ctx, handle: handle}, nil +} + +// Close destroys the underlying signal_protocol_store_context and releases +// the Go-side handle keeping the Store alive. Do not use the StoreContext +// (or anything built from it) afterward. +func (sc *StoreContext) Close() { + if sc.raw != nil { + C.signal_protocol_store_context_destroy(sc.raw) + sc.raw = nil + } + sc.handle.Delete() +} diff --git a/e2ee/omemo/libsignal/version_legacy.go b/e2ee/omemo/libsignal/version_legacy.go new file mode 100644 index 0000000..5185d45 --- /dev/null +++ b/e2ee/omemo/libsignal/version_legacy.go @@ -0,0 +1,37 @@ +//go:build signal_legacy + +package libsignal + +/* +#include +#include +#include +*/ +import "C" + +import "errors" + +// errModernOMEMOUnavailable is returned whenever the caller asks this +// legacy build to do something only libomemo-c can do. +var errModernOMEMOUnavailable = errors.New("libsignal: modern OMEMO (protocol v4) requires linking libomemo-c; this build links vanilla libsignal-protocol-c (build tag \"signal_legacy\")") + +// Vanilla libsignal-protocol-c has no session_builder_set_version / +// session_cipher_set_version - it only ever produces/consumes +// CIPHERTEXT_CURRENT_VERSION (protocol v3, legacy/siacs OMEMO) messages. +// Requesting ProtocolVersionLegacy is a no-op (that's the library's only +// behavior); requesting anything else is rejected rather than silently +// ignored. + +func setBuilderVersion(b *C.session_builder, version int) error { + if version != ProtocolVersionLegacy { + return errModernOMEMOUnavailable + } + return nil +} + +func setCipherVersion(c *C.session_cipher, version int) error { + if version != ProtocolVersionLegacy { + return errModernOMEMOUnavailable + } + return nil +} diff --git a/e2ee/omemo/libsignal/version_modern.go b/e2ee/omemo/libsignal/version_modern.go new file mode 100644 index 0000000..54ace5f --- /dev/null +++ b/e2ee/omemo/libsignal/version_modern.go @@ -0,0 +1,26 @@ +//go:build !signal_legacy + +package libsignal + +/* +#include +#include +#include +*/ +import "C" + +// setBuilderVersion and setCipherVersion select the wire protocol version +// (ProtocolVersionLegacy or ProtocolVersionModern) libomemo-c's +// session_builder_set_version/session_cipher_set_version support. See +// version_legacy.go for the vanilla-libsignal-protocol-c build, which has +// no such functions and only ever produces protocol v3. + +func setBuilderVersion(b *C.session_builder, version int) error { + C.session_builder_set_version(b, C.uint32_t(version)) + return nil +} + +func setCipherVersion(c *C.session_cipher, version int) error { + C.session_cipher_set_version(c, C.uint32_t(version)) + return nil +} diff --git a/staging.Dockerfile b/staging.Dockerfile index ca5a7e6..09fa037 100644 --- a/staging.Dockerfile +++ b/staging.Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.19-bullseye AS base RUN apt-get update -RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php +RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php libsignal-protocol-c-dev FROM base AS tdlib @@ -32,11 +32,14 @@ RUN --mount=type=cache,target=/gomod-cache \ FROM cache AS build ARG MAKEOPTS WORKDIR /src +# Bullseye only has the pre-fork libsignal-protocol-c packaged (libomemo-c +# is bookworm+) - GOTAGS=signal_legacy builds telegabber's OMEMO bindings +# against it, which only supports legacy/siacs OMEMO, not modern OMEMO 1/2. RUN --mount=type=bind,source=./,target=/src,rw \ --mount=type=cache,target=/go-cache \ --mount=type=cache,target=/gomod-cache \ --mount=type=cache,destination=/src/release \ - make ${MAKEOPTS} + make ${MAKEOPTS} GOTAGS=signal_legacy FROM build AS release RUN --mount=type=cache,destination=/src/release \