mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
Go bindings for libomemo-c and libsignal-protocol-c
This commit is contained in:
parent
b9bb367287
commit
ca0dd4361a
25 changed files with 1807 additions and 5 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
9
Makefile
9
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 ./...
|
||||
|
|
|
|||
13
README.md
13
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 `<prefix>/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.
|
||||
|
||||
|
|
|
|||
45
e2ee/omemo/libsignal/address.go
Normal file
45
e2ee/omemo/libsignal/address.go
Normal 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),
|
||||
}
|
||||
}
|
||||
50
e2ee/omemo/libsignal/buffer.go
Normal file
50
e2ee/omemo/libsignal/buffer.go
Normal 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))
|
||||
}
|
||||
293
e2ee/omemo/libsignal/callbacks.go
Normal file
293
e2ee/omemo/libsignal/callbacks.go
Normal 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) {}
|
||||
8
e2ee/omemo/libsignal/capability_legacy.go
Normal file
8
e2ee/omemo/libsignal/capability_legacy.go
Normal 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
|
||||
10
e2ee/omemo/libsignal/capability_modern.go
Normal file
10
e2ee/omemo/libsignal/capability_modern.go
Normal 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
|
||||
17
e2ee/omemo/libsignal/cgo_legacy.go
Normal file
17
e2ee/omemo/libsignal/cgo_legacy.go
Normal 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.
|
||||
19
e2ee/omemo/libsignal/cgo_modern.go
Normal file
19
e2ee/omemo/libsignal/cgo_modern.go
Normal 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.
|
||||
144
e2ee/omemo/libsignal/cipher.go
Normal file
144
e2ee/omemo/libsignal/cipher.go
Normal 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
|
||||
}
|
||||
89
e2ee/omemo/libsignal/context.go
Normal file
89
e2ee/omemo/libsignal/context.go
Normal 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")
|
||||
205
e2ee/omemo/libsignal/crypto.go
Normal file
205
e2ee/omemo/libsignal/crypto.go
Normal 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
|
||||
}
|
||||
23
e2ee/omemo/libsignal/deserialize_legacy.go
Normal file
23
e2ee/omemo/libsignal/deserialize_legacy.go
Normal 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)
|
||||
}
|
||||
27
e2ee/omemo/libsignal/deserialize_modern.go
Normal file
27
e2ee/omemo/libsignal/deserialize_modern.go
Normal 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
|
||||
}
|
||||
29
e2ee/omemo/libsignal/doc.go
Normal file
29
e2ee/omemo/libsignal/doc.go
Normal 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
|
||||
113
e2ee/omemo/libsignal/errors.go
Normal file
113
e2ee/omemo/libsignal/errors.go
Normal 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
|
||||
}
|
||||
268
e2ee/omemo/libsignal/keys.go
Normal file
268
e2ee/omemo/libsignal/keys.go
Normal 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
|
||||
}
|
||||
142
e2ee/omemo/libsignal/session.go
Normal file
142
e2ee/omemo/libsignal/session.go
Normal 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))
|
||||
}
|
||||
15
e2ee/omemo/libsignal/signedprekey_legacy.go
Normal file
15
e2ee/omemo/libsignal/signedprekey_legacy.go
Normal 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
|
||||
}
|
||||
17
e2ee/omemo/libsignal/signedprekey_modern.go
Normal file
17
e2ee/omemo/libsignal/signedprekey_modern.go
Normal 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))
|
||||
}
|
||||
204
e2ee/omemo/libsignal/store.go
Normal file
204
e2ee/omemo/libsignal/store.go
Normal 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()
|
||||
}
|
||||
37
e2ee/omemo/libsignal/version_legacy.go
Normal file
37
e2ee/omemo/libsignal/version_legacy.go
Normal 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
|
||||
}
|
||||
26
e2ee/omemo/libsignal/version_modern.go
Normal file
26
e2ee/omemo/libsignal/version_modern.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Reference in a new issue