Go bindings for libomemo-c and libsignal-protocol-c

This commit is contained in:
Bohdan Horbeshko 2026-07-28 00:38:46 -04:00
parent b9bb367287
commit ca0dd4361a
25 changed files with 1807 additions and 5 deletions

View file

@ -1,7 +1,7 @@
FROM golang:1.19-bookworm AS base FROM golang:1.19-bookworm AS base
RUN apt-get update 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 FROM base AS tdlib

View file

@ -5,12 +5,17 @@ TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551"
VERSION := "v2.0.0-dev" VERSION := "v2.0.0-dev"
MAKEOPTS := "-j4" 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: all:
mkdir -p release 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: 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: lint:
$(GOPATH)/bin/golint ./... $(GOPATH)/bin/golint ./...

View file

@ -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. * 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 `<prefix>/lib/pkgconfig`).
* Install Go (tested with 1.13, but may work with earlier versions too). * 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. * Open the source dir in a new shell (to make sure that `$GOPATH` works) and run `make`. Dependencies will be installed automatically.

View file

@ -0,0 +1,45 @@
package libsignal
/*
#include <signal_protocol.h>
*/
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),
}
}

View file

@ -0,0 +1,50 @@
package libsignal
/*
#include <signal_protocol.h>
*/
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))
}

View file

@ -0,0 +1,293 @@
package libsignal
/*
#include <signal_protocol.h>
*/
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) {}

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -0,0 +1,144 @@
package libsignal
/*
#include <signal_protocol.h>
#include <session_cipher.h>
#include <protocol.h>
*/
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 <key> vs a <key prekey="true">).
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
}

View file

@ -0,0 +1,89 @@
package libsignal
/*
#include <signal_protocol.h>
#include <stdint.h>
// 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")

View file

@ -0,0 +1,205 @@
package libsignal
/*
#include <signal_protocol.h>
*/
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
}

View file

@ -0,0 +1,23 @@
//go:build signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <protocol.h>
*/
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)
}

View file

@ -0,0 +1,27 @@
//go:build !signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <protocol.h>
*/
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
}

View file

@ -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 <encrypted> 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

View file

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

View file

@ -0,0 +1,268 @@
package libsignal
/*
#include <signal_protocol.h>
#include <key_helper.h>
#include <session_pre_key.h>
#include <ratchet.h>
#include <curve.h>
*/
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
}

View file

@ -0,0 +1,142 @@
package libsignal
/*
#include <signal_protocol.h>
#include <session_builder.h>
#include <session_pre_key.h>
#include <curve.h>
*/
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))
}

View file

@ -0,0 +1,15 @@
//go:build signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <session_pre_key.h>
*/
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
}

View file

@ -0,0 +1,17 @@
//go:build !signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <session_pre_key.h>
*/
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))
}

View file

@ -0,0 +1,204 @@
package libsignal
/*
#include <signal_protocol.h>
#include <stdint.h>
// 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()
}

View file

@ -0,0 +1,37 @@
//go:build signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <session_builder.h>
#include <session_cipher.h>
*/
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
}

View file

@ -0,0 +1,26 @@
//go:build !signal_legacy
package libsignal
/*
#include <signal_protocol.h>
#include <session_builder.h>
#include <session_cipher.h>
*/
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
}

View file

@ -1,7 +1,7 @@
FROM golang:1.19-bullseye AS base FROM golang:1.19-bullseye AS base
RUN apt-get update 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 FROM base AS tdlib
@ -32,11 +32,14 @@ RUN --mount=type=cache,target=/gomod-cache \
FROM cache AS build FROM cache AS build
ARG MAKEOPTS ARG MAKEOPTS
WORKDIR /src 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 \ RUN --mount=type=bind,source=./,target=/src,rw \
--mount=type=cache,target=/go-cache \ --mount=type=cache,target=/go-cache \
--mount=type=cache,target=/gomod-cache \ --mount=type=cache,target=/gomod-cache \
--mount=type=cache,destination=/src/release \ --mount=type=cache,destination=/src/release \
make ${MAKEOPTS} make ${MAKEOPTS} GOTAGS=signal_legacy
FROM build AS release FROM build AS release
RUN --mount=type=cache,destination=/src/release \ RUN --mount=type=cache,destination=/src/release \