Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-08-22 20:04:56 -04:00

Compare commits

..
Author SHA1 Message Date
Adam Van Ymeren
bb9d7f3b92 capabilities: advertise support for editing room title/avatar/members 2025-10-26 14:33:48 -07:00
145 changed files with 7559 additions and 18013 deletions

View file

@ -7,12 +7,10 @@ type: Bug
---
<!-- Include relevant logs, the bridge version and other important details here -->
<!--
Remember to include relevant logs, the bridge version and any other details.
### Checklist
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!signal version` command output is: ``
It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
ignored or closed immediately.
-->

View file

@ -11,14 +11,14 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
go-version: ["1.24", "1.25"]
name: Lint ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
@ -40,14 +40,14 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.25", "1.26"]
name: Test ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
go-version: ["1.24", "1.25"]
name: Test ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true

View file

@ -17,7 +17,7 @@ jobs:
lock-stale:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
- uses: dessant/lock-threads@v5
id: lock
with:
issue-inactive-days: 90

View file

@ -9,7 +9,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.4
rev: v1.0.0-rc.2
hooks:
- id: go-imports
exclude: "pb\\.go$"

View file

@ -1,93 +1,8 @@
# v26.08
# v25.11 (unreleased)
* Updated libsignal to v0.100.0
* Added support for handling own profile key changes.
* Changed attachment bridging behavior to match Signal Desktop.
* Fixed edited messages being bridged twice if certain race conditions occurred.
# v26.07
* Updated Docker image to Alpine 3.24.
* Updated libsignal to v0.97.2.
* Added automatic retry when sender key send fails due to missing session.
* Fixed changing poll votes from Matrix.
# v26.06
* Updated libsignal to v0.94.4.
* Updated mrenclave to fix looking up phone numbers.
* Changed knock accept handling to auto-join the ghost user afterwards.
# v26.05
* Updated libsignal to v0.93.2.
* Added support for importing sticker packs from Signal.
# v26.04
* Updated libsignal to v0.92.1.
* Added support for admin message deletes from Signal.
* Added support for binary service IDs in storage service.
* Fixed `private_chat_portal_meta` option not setting DM room names correctly.
* Fixed panic if user is logged out during initial chat sync.
* Fixed avatar upload failing when creating new Signal group.
# v26.03
* Switched to sending binary service ID fields in outgoing messages.
* Added support for roundtripping large attachments via disk to avoid keeping
the entire file in memory during en/decryption.
# v26.02.2
* Added support for more new binary service ID fields.
# v26.02.1
* Updated libsignal to v0.87.5.
* Added support for new binary service ID fields that Signal 8.0 switched to.
# v26.02
* Bumped minimum Go version to 1.25.
* Updated libsignal to v0.87.1.
* Added automatic recovery for the session not found error from libsignal.
* Fixed sender key state not being cleared on logout properly.
# v26.01
* Updated libsignal to v0.86.12.
* Changed automatic contact list sync option to only sync every 3 days rather
than on every restart.
* Fixed sending messages to groups with no other registered members.
* Fixed sender key sends failing if some users had changed devices.
* Fixed timestamps of outgoing typing notifications in DMs.
# v25.12
* Updated libsignal to v0.86.8.
* Updated Docker image to Alpine 3.23.
* Added support for dropping incoming DMs from blocked contacts on Signal.
* Added support for sender key encryption when sending to groups, which makes
sending much faster and enables sending typing notifications.
* Added support for encryption retry receipts.
* Fixed bugs with handling poll votes.
* Fixed history transfer option not showing up when pairing with Signal Android.
* Fixed nicknames being cleared not being bridged
(thanks to [@Enzime] in [#623]).
[#623]: https://github.com/mautrix/signal/pull/623
[@Enzime]: https://github.com/Enzime
# v25.11
* Updated libsignal to v0.86.4.
* Added support for bridging invite state in groups for phone number invites.
* Added support for polls.
* Fixed PNI signature not being sent when replying to message requests.
* Fixed unnecessary repeating error notices when Signal is down.
* Fixed sticker size metadata on Matrix not matching how native Signal Desktop
renders them.
# v25.10

View file

@ -1,17 +1,18 @@
# -- Build libsignal (with Rust) --
FROM rust:1-alpine AS rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev protobuf-dev
FROM rust:1-alpine as rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
WORKDIR /build
# Copy all files needed for Rust build, and no Go files
COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
COPY build-rust.sh .
ARG DBG=0
RUN ./build-rust.sh
# -- Build mautrix-signal (with Go) --
FROM golang:1-alpine3.24 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev zlib-dev
FROM golang:1-alpine3.22 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev
WORKDIR /build
# Copy all files needed for Go build, and no Rust files
@ -25,14 +26,20 @@ COPY pkg/connector/. pkg/connector/.
COPY cmd/. cmd/.
COPY .git .git
ARG DBG=0
ENV LIBRARY_PATH=.
COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./
RUN <<EOF
if [ "$DBG" = 1 ]; then
go install github.com/go-delve/delve/cmd/dlv@latest
else
touch /go/bin/dlv
fi
EOF
RUN ./build-go.sh
# -- Run mautrix-signal --
FROM alpine:3.24
FROM alpine:3.22
ENV UID=1337 \
GID=1337
@ -41,6 +48,11 @@ RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go olm
COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal
COPY --from=go-builder /build/docker-run.sh /docker-run.sh
COPY --from=go-builder /go/bin/dlv /usr/bin/dlv
VOLUME /data
ARG DBG
ARG DBGWAIT=0
ENV DBG=${DBG} DBGWAIT=${DBGWAIT}
RUN echo "Debug mode: DBG=${DBG} DBGWAIT=${DBGWAIT}"
CMD ["/docker-run.sh"]

View file

@ -1,6 +1,6 @@
ARG DOCKER_HUB="docker.io"
FROM ${DOCKER_HUB}/alpine:3.24
FROM ${DOCKER_HUB}/alpine:3.22
ENV UID=1337 \
GID=1337

View file

@ -5,7 +5,6 @@
* [x] Text
* [x] Formatting
* [x] Mentions
* [x] Polls
* [x] Media
* [x] Images
* [x] Audio files
@ -35,7 +34,6 @@
* [x] Text
* [x] Formatting
* [x] Mentions
* [x] Polls
* [ ] Media
* [x] Images
* [x] Voice notes
@ -67,8 +65,8 @@
* [ ] Delivery receipts (there's no good way to bridge these)
* [x] Disappearing messages
* Misc
* [x] Automatic portal creation
* [x] After login
* [ ] Automatic portal creation
* [ ] After login
* [x] When receiving message
* [x] Linking as secondary device
* [ ] Registering as primary device

View file

@ -1,2 +1,9 @@
#!/bin/sh
BINARY_NAME=mautrix-signal go tool maubuild "$@"
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
GO_LDFLAGS="-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
if [ "$DBG" = 1 ]; then
GO_GCFLAGS='all=-N -l'
else
GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
fi
go build -gcflags="$GO_GCFLAGS" -ldflags="$GO_LDFLAGS" -o mautrix-signal "$@" ./cmd/mautrix-signal

View file

@ -1,3 +1,10 @@
#!/bin/sh
# TODO fix linking with debug library
#if [ "$DBG" != 1 ]; then
# RUST_PROFILE=release
#else
# RUST_PROFILE=dev
#fi
RUST_PROFILE=release
git submodule update --init
cd pkg/libsignalgo/libsignal && RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" cargo build -p libsignal-ffi --profile=release
cd pkg/libsignalgo/libsignal && RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" cargo build -p libsignal-ffi --profile=$RUST_PROFILE

View file

@ -17,12 +17,9 @@
package main
import (
"fmt"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
"go.mau.fi/mautrix-signal/pkg/connector"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
// Information to find out exactly which commit the bridge was built from.
@ -37,14 +34,13 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.",
Version: "26.08",
Version: "25.10",
SemCalVer: true,
Connector: &connector.SignalConnector{},
}
func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)

45
go.mod
View file

@ -1,51 +1,48 @@
module go.mau.fi/mautrix-signal
go 1.25.0
go 1.24.0
toolchain go1.26.6
tool go.mau.fi/util/cmd/maubuild
toolchain go1.25.3
require (
github.com/coder/websocket v1.8.15
github.com/coder/websocket v1.8.14
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
github.com/google/uuid v1.6.0
github.com/mattn/go-pointer v0.0.1
github.com/rs/zerolog v1.35.1
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.10.0
golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
google.golang.org/protobuf v1.36.12
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.2
golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b
golang.org/x/net v0.46.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.30.0
maunium.net/go/mautrix v0.25.3-0.20251023124911-1be49d53e4f3
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect

84
go.sum
View file

@ -1,16 +1,17 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -22,19 +23,23 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@ -42,46 +47,45 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM=
go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -91,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=
maunium.net/go/mautrix v0.25.3-0.20251023124911-1be49d53e4f3 h1:C+inXVPJspCtjiLIP64npDR01o5Fs9bE9lg3jr0PzS4=
maunium.net/go/mautrix v0.25.3-0.20251023124911-1be49d53e4f3/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI=

View file

@ -151,7 +151,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
if dm == nil {
continue
}
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, senderACI, s.Main.Bridge.Bot, dm, attMap)
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, s.Main.Bridge.Bot, dm, attMap)
convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions))
for _, reaction := range reactions {
reactionSenderACI, err := getRecipientACI(reaction.AuthorId)
@ -187,7 +187,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
CompleteCallback: func() {
// When reaching the last backwards backfill batch, delete the chat from the backup store.
// If backwards backfilling isn't enabled, delete immediately after the first backfill request.
if (!params.Forward && len(items) < params.Count) || !s.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
if (!params.Forward && len(items) < params.Count) || (!s.Main.Bridge.Config.Backfill.Queue.Enabled && !s.Main.Bridge.Config.Backfill.WillPaginateManually) {
err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")

View file

@ -25,7 +25,6 @@ import (
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
@ -38,7 +37,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.signal.capabilities.2026_07_22"
base := "fi.mau.signal.capabilities.2025_08_25"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -77,7 +76,6 @@ var signalCaps = &event.RoomFeatures{
"image/jpeg": event.CapLevelFullySupported,
"image/webp": event.CapLevelFullySupported,
"image/bmp": event.CapLevelFullySupported,
"image/avif": event.CapLevelFullySupported,
},
MaxWidth: 4096,
MaxHeight: 4096,
@ -99,8 +97,6 @@ var signalCaps = &event.RoomFeatures{
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/aac": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported,
},
MaxSize: MaxFileSize,
},
@ -114,8 +110,7 @@ var signalCaps = &event.RoomFeatures{
},
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
// Signal clients will only render static webp, so apng is preferred
"image/webp": event.CapLevelPartialSupport,
"image/webp": event.CapLevelFullySupported,
"image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(),
@ -141,27 +136,9 @@ var signalCaps = &event.RoomFeatures{
MaxSize: MaxFileSize,
},
},
State: event.StateFeatureMap{
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
},
MemberActions: event.MemberFeatureMap{
event.MemberActionInvite: event.CapLevelFullySupported,
event.MemberActionRevokeInvite: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported,
event.MemberActionBan: event.CapLevelFullySupported,
event.MemberActionKick: event.CapLevelFullySupported,
},
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelFullySupported,
PollMaxOptions: 10,
PollOptionMaxLength: 100,
Poll: event.CapLevelRejected,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
@ -179,11 +156,10 @@ var signalCaps = &event.RoomFeatures{
ReadReceipts: true,
TypingNotifications: true,
DeleteChat: true,
MessageRequest: &event.MessageRequestFeatures{
AcceptWithMessage: event.CapLevelPartialSupport,
AcceptWithButton: event.CapLevelFullySupported,
},
// Room management capabilities
ManageMembers: true,
SetRoomAvatar: true,
SetRoomTitle: true,
}
var signalDisappearingCap = &event.DisappearingTimerCapability{
@ -191,16 +167,9 @@ var signalDisappearingCap = &event.DisappearingTimerCapability{
}
var signalCapsNoteToSelf *event.RoomFeatures
var signalCapsDM *event.RoomFeatures
func init() {
signalCapsDM = ptr.Clone(signalCaps)
signalCapsDM.ID = capID() + "+dm"
signalCapsDM.MemberActions = nil
signalCapsDM.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
}
signalCapsNoteToSelf = ptr.Clone(signalCapsDM)
signalCapsNoteToSelf = ptr.Clone(signalCaps)
signalCapsNoteToSelf.EditMaxAge = nil
signalCapsNoteToSelf.DeleteMaxAge = nil
signalCapsNoteToSelf.ID = capID() + "+note_to_self"
@ -209,8 +178,6 @@ func init() {
func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
return signalCapsNoteToSelf
} else if portal.RoomType == database.RoomTypeDM {
return signalCapsDM
}
return signalCaps
}
@ -220,7 +187,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@ -245,5 +211,6 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 11
// Bump capabilities version to resend updated room features
return 1, 6
}

View file

@ -212,7 +212,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
return nil, fmt.Errorf("error looking up number in local contact list: %w", err)
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
} else if recipient != nil {
aci = recipient.ACI
pni = recipient.PNI
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
@ -228,9 +228,6 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
}
aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
}
} else {
aci, pni = serviceID.ToACIAndPNI()
@ -326,13 +323,13 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
}
var avatarBytes []byte
var avatarMXC id.ContentURIString
if params.Avatar != nil && params.Avatar.URL != "" {
if params.Avatar != nil {
avatarMXC = params.Avatar.URL
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil)
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, params.Avatar.MSC3414File)
if err != nil {
return nil, fmt.Errorf("failed to download avatar: %w", err)
}
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to upload avatar: %w", err)
}
@ -362,7 +359,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
return nil, fmt.Errorf("failed to set portal room ID: %w", err)
}
}
resp, err := s.Client.CreateGroup(ctx, group)
resp, err := s.Client.CreateGroup(ctx, group, avatarBytes)
if err != nil {
return nil, fmt.Errorf("failed to create group: %w", err)
}
@ -414,7 +411,7 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
}
func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
namePtr := bridgev2.DefaultChatName
name := ""
topic := PrivateChatTopic
selfUser := s.makeEventSender(s.Client.Store.ACI)
members := &bridgev2.ChatMemberList{
@ -441,7 +438,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
var serviceID libsignalgo.ServiceID
var avatar *bridgev2.Avatar
if recipient.ACI == uuid.Nil {
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient))
name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else {
if backupChat == nil {
@ -453,7 +450,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
}
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI {
namePtr = ptr.Ptr(NoteToSelfName)
name = NoteToSelfName
avatar = &bridgev2.Avatar{
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
@ -474,14 +471,14 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
return &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(serviceID),
PortalInfo: &bridgev2.ChatInfo{
Name: namePtr,
Name: &name,
Avatar: avatar,
Topic: &topic,
Members: members,
Type: ptr.Ptr(database.RoomTypeDM),
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
CanBackfill: backupChat != nil,
ExtraUpdates: updatePortalSyncMeta,
},
}

View file

@ -32,19 +32,10 @@ import (
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
func (s *SignalClient) stopChatSync() {
if cancel := s.cancelChatSync.Swap(nil); cancel != nil {
(*cancel)()
}
}
func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) {
defer cancel()
func (s *SignalClient) syncChats(ctx context.Context) {
if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced {
return
}
if s.Client.Store.EphemeralBackupKey != nil {
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
meta, err := s.Client.WaitForTransfer(ctx)
@ -74,22 +65,10 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
}
zerolog.Ctx(ctx).Info().Int("chat_count", len(chats)).Msg("Fetched chats to sync from database")
for _, chat := range chats {
if ctx.Err() != nil {
zerolog.Ctx(ctx).Debug().
AnErr("ctx_err", ctx.Err()).
Msg("Context cancelled while syncing chats, stopping")
return
}
recipient, err := s.Client.Store.BackupStore.GetBackupRecipient(ctx, chat.RecipientId)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat")
continue
} else if recipient == nil {
zerolog.Ctx(ctx).Warn().
Uint64("backup_chat_id", chat.Id).
Uint64("backup_recipient_id", chat.RecipientId).
Msg("No recipient found for chat")
continue
}
resyncEvt := &simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
@ -151,10 +130,7 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
groupID := types.GroupIdentifier(base64.StdEncoding.EncodeToString(rawGroupID[:]))
groupInfo, err := s.getGroupInfo(ctx, groupID, dest.Group.GetSnapshot().GetVersion(), chat)
if err != nil {
zerolog.Ctx(ctx).Err(err).
Uint64("recipient_id", recipient.Id).
Stringer("group_id", groupID).
Msg("Failed to get full group info")
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full group info")
continue
}
resyncEvt.PortalKey = s.makePortalKey(string(groupID))

View file

@ -19,7 +19,6 @@ package connector
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/rs/zerolog"
@ -27,7 +26,6 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
@ -41,13 +39,11 @@ type SignalClient struct {
Ghost *bridgev2.Ghost
queueEmptyWaiter *exsync.Event
cancelChatSync atomic.Pointer[context.CancelFunc]
}
var (
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@ -78,27 +74,18 @@ func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType b
}
}
func (s *SignalClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
return s.Main.MsgConv.DownloadImagePack(ctx, url)
}
func (s *SignalClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
return []*event.ImagePackMetadata{}, nil
}
func (s *SignalClient) LogoutRemote(ctx context.Context) {
if s.Client == nil {
return
}
s.stopChatSync()
err := s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
}
err = s.Client.StopReceiveLoops()
err := s.Client.StopReceiveLoops()
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout")
}
err = s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
}
err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store")
@ -189,7 +176,6 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
}
case signalmeow.SignalConnectionEventLoggedOut:
s.stopChatSync()
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
if err == nil {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
@ -288,7 +274,6 @@ func (s *SignalClient) Disconnect() {
if s.Client == nil {
return
}
s.stopChatSync()
err := s.Client.StopReceiveLoops()
if err != nil {
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
@ -296,20 +281,28 @@ func (s *SignalClient) Disconnect() {
}
func (s *SignalClient) postLoginConnect() {
ctx := s.UserLogin.Log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := s.UserLogin.Log.WithContext(context.Background())
// TODO it would be more proper to only connect after syncing,
// but currently syncing will fetch group info online, so it has to be connected.
s.tryConnect(ctx, 0, false)
if s.Client.Store.EphemeralBackupKey != nil {
go func() {
s.syncChats(ctx)
if s.Client.Store.MasterKey != nil {
s.Client.SyncStorage(ctx)
}
}()
} else if s.Client.Store.MasterKey != nil {
go s.Client.SyncStorage(ctx)
}
}
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSync bool) {
if ctx.Err() != nil {
zerolog.Ctx(ctx).Debug().
Int("retry_count", retryCount).
AnErr("ctx_err", ctx.Err()).
Msg("Context is canceled, not trying to connect")
return
}
if retryCount == 0 {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, doSync bool) {
err := s.Client.RegisterCapabilities(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
} else {
zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
}
ch, err := s.Client.StartReceiveLoops(ctx)
if err != nil {
@ -320,39 +313,12 @@ func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSy
retryInSeconds = 150
}
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
select {
case <-time.After(time.Duration(retryInSeconds) * time.Second):
case <-ctx.Done():
zerolog.Ctx(ctx).Info().Msg("Context canceled, exit tryConnect")
return
}
s.tryConnect(ctx, retryCount+1, noLoginSync)
return
}
syncCtx, cancel := context.WithCancel(ctx)
if oldCancel := s.cancelChatSync.Swap(&cancel); oldCancel != nil {
(*oldCancel)()
}
time.Sleep(time.Duration(retryInSeconds) * time.Second)
s.tryConnect(ctx, retryCount+1, doSync)
} else {
go s.bridgeStateLoop(ch)
if noLoginSync {
go s.syncChats(syncCtx, cancel)
} else {
// TODO it would be more proper to only connect after syncing,
// but currently syncing will fetch group info online, so it has to be connected.
if s.Client.Store.EphemeralBackupKey != nil {
go func() {
if s.Client.Store.MasterKey != nil {
s.Client.SyncStorage(ctx)
} else {
s.UserLogin.Log.Warn().Msg("No master key for storage sync before backup sync")
}
s.syncChats(syncCtx, cancel)
}()
} else {
cancel()
if s.Client.Store.MasterKey != nil {
go s.Client.SyncStorage(ctx)
}
if doSync {
go s.syncChats(ctx)
}
}
}

View file

@ -1,73 +0,0 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2025 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"errors"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/networkid"
"go.mau.fi/mautrix-signal/pkg/signalid"
)
var CmdDiscardSenderKey = &commands.FullHandler{
Func: fnDiscardSenderKey,
Name: "discard-sender-key",
Help: commands.HelpMeta{
Section: commands.HelpSectionChats,
Description: "Discard the Signal-side sender key in the current group",
Args: "[_login ID_]",
},
RequiresPortal: true,
RequiresLogin: true,
}
func fnDiscardSenderKey(ce *commands.Event) {
_, groupID, _ := signalid.ParsePortalID(ce.Portal.ID)
if groupID == "" {
ce.Reply("This command can only be used in group chat portals")
return
}
var login *bridgev2.UserLogin
if len(ce.Args) > 0 {
login = ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if login == nil || login.UserMXID != ce.User.MXID {
ce.Reply("Login not found")
return
}
} else {
var err error
login, _, err = ce.Portal.FindPreferredLogin(ce.Ctx, ce.User, false)
if errors.Is(err, bridgev2.ErrNotLoggedIn) {
ce.Reply("You're not logged in in this portal")
return
} else if err != nil {
ce.Log.Err(err).Msg("Failed to find preferred login for portal")
ce.Reply("Failed to find preferred login for portal")
return
}
}
distributionID, err := login.Client.(*SignalClient).Client.ResetSenderKey(ce.Ctx, groupID)
if err != nil {
ce.Log.Err(err).Msg("Failed to reset sender key")
ce.Reply("Failed to reset sender key")
} else {
ce.Reply("Reset sender key with distribution ID %s", distributionID)
}
}

View file

@ -42,7 +42,6 @@ type SignalConfig struct {
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"`
DisappearViewOnce bool `yaml:"disappear_view_once"`
ExtEvPolls bool `yaml:"extev_polls"`
displaynameTemplate *template.Template `yaml:"-"`
}
@ -104,7 +103,6 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "note_to_self_avatar")
helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "disappear_view_once")
helper.Copy(up.Bool, "extev_polls")
}
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {

View file

@ -24,19 +24,15 @@ import (
"github.com/google/uuid"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exhttp"
"go.mau.fi/util/exsync"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-signal/pkg/msgconv"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
type SignalConnector struct {
@ -67,8 +63,6 @@ func (s *SignalConnector) Init(bridge *bridgev2.Bridge) {
s.MsgConv = msgconv.NewMessageConverter(bridge)
s.MsgConv.LocationFormat = s.Config.LocationFormat
s.MsgConv.DisappearViewOnce = s.Config.DisappearViewOnce
s.MsgConv.ExtEvPolls = s.Config.ExtEvPolls
bridge.Commands.(*commands.Processor).AddHandlers(CmdDiscardSenderKey)
}
func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
@ -76,7 +70,6 @@ func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
}
func (s *SignalConnector) Start(ctx context.Context) error {
s.ResetHTTPTransport()
err := s.Store.Upgrade(ctx)
if err != nil {
return bridgev2.DBUpgradeError{Err: err, Section: "signalmeow"}
@ -84,26 +77,6 @@ func (s *SignalConnector) Start(ctx context.Context) error {
return nil
}
func (s *SignalConnector) ResetHTTPTransport() {
settings := exhttp.SensibleClientSettings
hs, ok := s.Bridge.Matrix.(bridgev2.MatrixConnectorWithHTTPSettings)
if ok {
settings = hs.GetHTTPClientSettings()
}
oldClient := web.SignalHTTPClient
web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile()
oldClient.CloseIdleConnections()
}
func (s *SignalConnector) ResetNetworkConnections() {
for _, login := range s.Bridge.GetAllCachedUserLogins() {
c := login.Client.(*SignalClient)
if c.Client != nil {
c.Client.ForceReconnect()
}
}
}
func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error {
aci, err := uuid.Parse(string(login.ID))
if err != nil {
@ -120,13 +93,13 @@ func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.Use
queueEmptyWaiter: exsync.NewEvent(),
}
if device != nil {
sc.Client = signalmeow.NewClient(
device,
sc.UserLogin.Log.With().Str("component", "signalmeow").Logger(),
sc.handleSignalEvent,
)
sc.Client.SyncContactsOnConnect = s.Config.SyncContactsOnStartup &&
time.Since(login.Metadata.(*signalid.UserLoginMetadata).LastContactSync.Time) > 3*24*time.Hour
sc.Client = &signalmeow.Client{
Store: device,
Log: sc.UserLogin.Log.With().Str("component", "signalmeow").Logger(),
EventHandler: sc.handleSignalEvent,
SyncContactsOnConnect: s.Config.SyncContactsOnStartup,
}
}
login.Client = sc
return nil

View file

@ -4,7 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"os"
"io"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
@ -29,7 +29,6 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to parse direct media id: %w", err)
}
var rawDataResp []byte
switch info := info.(type) {
case *signalid.DirectMediaAttachment:
log.Info().
@ -42,15 +41,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
Uint32("size", info.Size).
Msg("Direct downloading attachment")
return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
_, err := signalmeow.DownloadAttachment(
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size, w,
return &mediaproxy.GetMediaResponseCallback{
Callback: func(w io.Writer) (int64, error) {
data, err := signalmeow.DownloadAttachment(
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size,
)
if err != nil {
return nil, err
log.Err(err).Msg("Direct download failed")
return 0, err
}
return &mediaproxy.FileMeta{}, nil
_, err = w.Write(data)
return int64(info.Size), err
},
}, nil
case *signalid.DirectMediaGroupAvatar:
@ -76,11 +78,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to to get group master key: %w", err)
}
rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
return &mediaproxy.GetMediaResponseCallback{
Callback: func(w io.Writer) (int64, error) {
data, err := client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
return 0, err
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
case *signalid.DirectMediaProfileAvatar:
log.Info().
Stringer("user_id", info.UserID).
@ -104,27 +113,19 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("profile key not found")
}
rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
return &mediaproxy.GetMediaResponseCallback{
Callback: func(w io.Writer) (int64, error) {
data, err := client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
return 0, err
}
case *signalid.DirectMediaSticker:
log.Info().
Hex("pack_id", info.PackID).
Uint32("sticker_id", info.StickerID).
Msg("Direct downloading sticker")
rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
}
if rawDataResp == nil {
return nil, fmt.Errorf("unexpected fallthrough with no data")
}
return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
}

View file

@ -24,5 +24,3 @@ note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'
# Should view-once messages disappear shortly after sending a read receipt on Matrix?
disappear_view_once: false
# Should polls be sent using unstable MSC3381 event types?
extev_polls: false

View file

@ -98,7 +98,7 @@ func inviteLinkToJoinRule(inviteLinkAccess signalmeow.AccessControl) event.JoinR
}
func (s *SignalClient) getGroupInfo(ctx context.Context, groupID types.GroupIdentifier, minRevision uint32, backupChat *store.BackupChat) (*bridgev2.ChatInfo, error) {
groupInfo, _, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
groupInfo, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
if err != nil {
return nil, fmt.Errorf("failed to retrieve group by id: %w", err)
}

View file

@ -21,7 +21,6 @@ import (
"crypto/sha256"
"errors"
"fmt"
"slices"
"strconv"
"time"
@ -53,9 +52,6 @@ var (
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DeleteChatHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.PollHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.MessageRequestAcceptingNetworkAPI = (*SignalClient)(nil)
)
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
@ -74,17 +70,17 @@ func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.Porta
Int("failed_to_send_to_count", len(result.FailedToSendTo)).
Int("successfully_sent_to_count", len(result.SuccessfullySentTo)).
Logger()
if len(result.FailedToSendTo) > 0 {
log.Error().Msg("Failed to send event to some members of Signal group")
}
if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 {
log.Debug().Msg("No successes or failures - Probably sent to myself")
} else if len(result.SuccessfullySentTo) == 0 {
log.Error().Msg("Failed to send event to all members of Signal group")
return errors.New("failed to send to any members of Signal group")
} else if len(result.SuccessfullySentTo) < totalRecipients {
if len(result.FailedToSendTo) > 0 {
log.Warn().Msg("Failed to send event to some members of Signal group")
} else {
log.Warn().Msg("Only sent event to some members of Signal group")
}
} else {
log.Debug().Msg("Sent event to all members of Signal group")
}
@ -113,31 +109,16 @@ func getTimestampForEvent(txnID networkid.RawTransactionID, evt *event.Event, or
}
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted, err := s.Main.MsgConv.ToSignal(
ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo,
ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, msg.ReplyTo,
)
if err != nil {
return nil, err
}
return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
})
}
func (s *SignalClient) doSendMessage(
ctx context.Context,
msg *bridgev2.MatrixMessage,
converted *signalpb.DataMessage,
meta *signalid.MessageMetadata,
) (*bridgev2.MatrixMessageResponse, error) {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts
if meta == nil {
meta = &signalid.MessageMetadata{}
}
msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.AddPendingToIgnore(networkid.TransactionID(msgID))
err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted))
err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{DataMessage: converted})
if err != nil {
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
}
@ -145,7 +126,9 @@ func (s *SignalClient) doSendMessage(
ID: msgID,
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
Timestamp: time.UnixMilli(int64(ts)),
Metadata: meta,
Metadata: &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
},
}
return &bridgev2.MatrixMessageResponse{
DB: dbMsg,
@ -167,56 +150,24 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
return fmt.Errorf("failed to get message reply target: %w", err)
}
}
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, replyTo)
if err != nil {
return err
}
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapEditMessage(&signalpb.EditMessage{
err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{EditMessage: &signalpb.EditMessage{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
DataMessage: converted,
}))
}})
if err != nil {
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
}
prevID := msg.EditTarget.ID
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
msg.EditTarget.EditCount++
if prevID != msg.EditTarget.ID {
err = s.Main.Bridge.DB.Message.Update(ctx, msg.EditTarget)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save message after editing")
} else {
saveEditStub(ctx, s.Main.Bridge, prevID, msg.EditTarget)
}
}
return nil
}
// saveEditStub saves a placeholder message row pointing at the pre-edit ID of a message, such that
// duplicate checks on incoming edits find it and are dropped. This is necessary because the first
// time we see an edit it modifies the ID in place.
func saveEditStub(ctx context.Context, bridge *bridgev2.Bridge, prevID networkid.MessageID, target *database.Message) {
stub := &database.Message{
ID: prevID,
PartID: editStubPartID,
Room: target.Room,
SenderID: target.SenderID,
SenderMXID: target.SenderMXID,
Timestamp: target.Timestamp,
}
stub.SetFakeMXID()
err := bridge.DB.Message.Insert(ctx, stub)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Str("prev_message_id", string(prevID)).
Str("message_id", string(target.ID)).
Msg("Failed to save stub row for pre-edit message ID")
}
}
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
return bridgev2.MatrixReactionPreResponse{
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
@ -231,16 +182,19 @@ func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.M
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
}
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts),
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Reaction: &signalpb.DataMessage_Reaction{
Emoji: proto.String(msg.PreHandleResp.Emoji),
Remove: proto.Bool(false),
TargetAuthorAciBinary: targetAuthorACI[:],
TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}))
},
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil {
return nil, err
}
@ -253,16 +207,19 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
return fmt.Errorf("failed to parse target message ID: %w", err)
}
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts),
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Reaction: &signalpb.DataMessage_Reaction{
Emoji: proto.String(msg.TargetReaction.Emoji),
Remove: proto.Bool(true),
TargetAuthorAciBinary: targetAuthorACI[:],
TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}))
},
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil {
return err
}
@ -277,12 +234,15 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg
return fmt.Errorf("cannot delete other people's messages")
}
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts),
Delete: &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}))
},
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil {
return err
}
@ -342,21 +302,18 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
}
func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error {
userID, groupID, err := signalid.ParsePortalID(typing.Portal.ID)
userID, _, err := signalid.ParsePortalID(typing.Portal.ID)
if err != nil {
return err
}
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
// Only send typing notifications in DMs for now
// Sending efficiently to groups requires implementing the proper SenderKey stuff first
if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI {
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
result := s.Client.SendMessage(ctx, userID, typingMessage)
if !result.WasSuccessful {
return result.Error
}
} else if groupID != "" {
_, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage)
if err != nil {
return err
}
}
return nil
}
@ -409,7 +366,7 @@ func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2
return false, fmt.Errorf("failed to download avatar: %w", err)
}
avatarHash = sha256.Sum256(data)
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "")
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID)
if err != nil {
return false, fmt.Errorf("failed to reupload avatar: %w", err)
}
@ -428,24 +385,22 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.
}, nil)
}
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
var targetIntent bridgev2.MatrixAPI
var targetSignalID libsignalgo.ServiceID
var err error
if msg.Portal.RoomType == database.RoomTypeDM {
//TODO: this probably needs to revert some changes and clean up the portal on leaves
switch msg.Type {
case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm")
return false, fmt.Errorf("cannot invite additional user to dm")
default:
return nil, nil
return false, nil
}
}
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
if err != nil {
return nil, fmt.Errorf("failed to parse target signal id: %w", err)
return false, fmt.Errorf("failed to parse target signal id: %w", err)
}
switch target := msg.Target.(type) {
case *bridgev2.Ghost:
@ -455,12 +410,12 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
if targetIntent == nil {
ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
if err != nil {
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
return false, fmt.Errorf("failed to get ghost for user: %w", err)
}
targetIntent = ghost.Intent
}
default:
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
return false, fmt.Errorf("cannot get target intent: unknown type: %T", target)
}
log := zerolog.Ctx(ctx).With().
Str("From Membership", string(msg.Type.From)).
@ -480,7 +435,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
switch msg.Type {
case bridgev2.AcceptInvite:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept invite for non-ACI service ID")
return false, fmt.Errorf("can't accept invite for non-ACI service ID")
}
gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{
ACI: targetSignalID.UUID,
@ -489,7 +444,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
case bridgev2.Leave, bridgev2.Kick:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't kick non-ACI service ID")
return false, fmt.Errorf("can't kick non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.Invite:
@ -525,7 +480,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
// }}
case bridgev2.AcceptKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept knock from non-ACI service ID")
return false, fmt.Errorf("can't accept knock from non-ACI service ID")
}
gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{
ACI: targetSignalID.UUID,
@ -533,7 +488,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
}}
case bridgev2.RetractKnock, bridgev2.RejectKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't reject knock from non-ACI service ID")
return false, fmt.Errorf("can't reject knock from non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft:
@ -544,39 +499,40 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
switch msg.Type {
case bridgev2.BanJoined:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
return false, fmt.Errorf("can't ban joined non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanInvited:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
case bridgev2.BanKnocked:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
return false, fmt.Errorf("can't ban knocked non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
}
case bridgev2.Unban:
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID}
default:
return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To)
log.Debug().Msg("unsupported membership change")
return false, nil
}
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil || groupID == "" {
return nil, err
return false, err
}
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil {
return nil, err
return false, err
}
if (msg.Type == bridgev2.Invite || msg.Type == bridgev2.AcceptKnock) && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
if err != nil {
return nil, err
return false, err
}
}
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return nil, nil
return true, nil
}
func plToRole(pl int) signalmeow.GroupMemberRole {
@ -710,11 +666,13 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
})
} else {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
res := s.Client.SendMessage(ctx, userID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: ptr.Ptr(ts),
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
}))
},
})
if !res.WasSuccessful {
return false, res.Error
}
@ -722,231 +680,3 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
return true, nil
}
}
func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
if msg.Content.FromMessageRequest {
// TODO block and delete support?
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE)
if err != nil {
return fmt.Errorf("failed to send message request delete sync: %w", err)
}
}
// Build ConversationIdentifier based on portal type
var conversationID *signalpb.ConversationIdentifier
if groupID == "" {
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadServiceIdBinary{
ThreadServiceIdBinary: userID.Bytes(),
},
}
} else {
gid, err := groupID.Bytes()
if err != nil {
return fmt.Errorf("failed to parse group ID: %w", err)
}
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadGroupId{
ThreadGroupId: gid[:],
},
}
}
// Retrieve most recent messages from the portal
var mostRecentMessages []*signalpb.AddressableMessage
dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(
ctx,
msg.Portal.PortalKey,
time.Now().Add(-30*24*time.Hour), // Last 30 days
time.Now(),
)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get recent messages for conversation delete")
} else if len(dbMessages) > 0 {
// Limit to the 5 most recent messages overall
limit := 5
startIdx := 0
if len(dbMessages) > limit {
startIdx = len(dbMessages) - limit
}
// Create AddressableMessage for most recent messages
for _, dbMsg := range dbMessages[startIdx:] {
senderACI, timestamp, err := signalid.ParseMessageID(dbMsg.ID)
if err != nil {
continue
}
mostRecentMessages = append(mostRecentMessages, &signalpb.AddressableMessage{
Author: &signalpb.AddressableMessage_AuthorServiceIdBinary{
AuthorServiceIdBinary: senderACI[:],
},
SentTimestamp: proto.Uint64(timestamp),
})
}
}
recipientID := s.Client.Store.ACIServiceID()
// Send DeleteForMe sync message to self
result := s.Client.SendMessage(ctx, recipientID, signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
Content: &signalpb.SyncMessage_DeleteForMe_{
DeleteForMe: &signalpb.SyncMessage_DeleteForMe{
ConversationDeletes: []*signalpb.SyncMessage_DeleteForMe_ConversationDelete{{
Conversation: conversationID,
MostRecentMessages: mostRecentMessages,
IsFullDelete: proto.Bool(true),
}},
},
},
}))
zerolog.Ctx(ctx).Debug().
Str("portal_id", string(msg.Portal.ID)).
Int("recent_messages_count", len(mostRecentMessages)).
Msg("Sent conversation deletion to Signal")
if !result.WasSuccessful {
return fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID)
}
return nil
}
func (s *SignalClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
optionNames := make([]string, len(msg.Content.PollStart.Answers))
optionIDs := make([]string, len(msg.Content.PollStart.Answers))
for i, option := range msg.Content.PollStart.Answers {
optionNames[i] = option.Text
optionIDs[i] = option.ID
}
converted := &signalpb.DataMessage{
PollCreate: &signalpb.DataMessage_PollCreate{
Question: ptr.Ptr(msg.Content.PollStart.Question.Text),
AllowMultiple: ptr.Ptr(msg.Content.PollStart.MaxSelections != 1),
Options: optionNames,
},
RequiredProtocolVersion: ptr.Ptr(uint32(signalpb.DataMessage_POLLS)),
}
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{
MatrixPollOptionIDs: optionIDs,
})
}
func (s *SignalClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
senderACI, msgTS, err := signalid.ParseMessageID(msg.VoteTo.ID)
if err != nil {
return nil, err
}
meta := msg.VoteTo.Metadata.(*signalid.MessageMetadata)
mxOptions := meta.MatrixPollOptionIDs
optionIndexes := make([]uint32, len(msg.Content.Response.Answers))
for i, answer := range msg.Content.Response.Answers {
if idx := slices.Index(mxOptions, answer); idx >= 0 {
optionIndexes[i] = uint32(idx)
} else if idx, err = strconv.Atoi(answer); err == nil && idx >= 0 {
optionIndexes[i] = uint32(idx)
} else {
return nil, fmt.Errorf("unknown poll answer ID: %s", answer)
}
}
if meta.VoteCount == nil {
meta.VoteCount = make(map[string]uint32)
}
meta.VoteCount[s.Client.Store.ACI.String()]++
err = s.Main.Bridge.DB.Message.Update(ctx, msg.VoteTo)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to update poll message with new vote count")
}
converted := &signalpb.DataMessage{
PollVote: &signalpb.DataMessage_PollVote{
TargetAuthorAciBinary: senderACI[:],
TargetSentTimestamp: &msgTS,
OptionIndexes: optionIndexes,
VoteCount: proto.Uint32(meta.VoteCount[s.Client.Store.ACI.String()]),
},
RequiredProtocolVersion: proto.Uint32(0),
}
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, nil)
}
func (s *SignalClient) syncMessageRequestResponse(
ctx context.Context,
portal *bridgev2.Portal,
respType signalpb.SyncMessage_MessageRequestResponse_Type,
) error {
userID, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil {
return err
}
accept := &signalpb.SyncMessage_MessageRequestResponse{
Type: respType.Enum(),
}
if groupID != "" {
gidBytes, err := groupID.Bytes()
if err != nil {
return fmt.Errorf("failed to parse group ID: %w", err)
}
accept.GroupId = gidBytes[:]
} else if userID.Type == libsignalgo.ServiceIDTypeACI {
accept.ThreadAciBinary = userID.UUID[:]
} else {
return fmt.Errorf("invalid portal ID for message request response: %s", portal.ID)
}
res := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(s.Client.Store.ACI), signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
Content: &signalpb.SyncMessage_MessageRequestResponse_{
MessageRequestResponse: accept,
},
}))
if !res.WasSuccessful {
return res.Error
}
return nil
}
func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg *bridgev2.MatrixAcceptMessageRequest) error {
userID, _, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT)
if err != nil {
return fmt.Errorf("failed to sync message request acceptance: %w", err)
}
if userID.Type == libsignalgo.ServiceIDTypeACI {
profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI)
if err != nil {
return fmt.Errorf("failed to get own profile key: %w", err)
}
var pniSig *signalpb.PniSignatureMessage
if s.Client.Store.AccountRecord.GetPhoneNumberSharingMode() == signalpb.AccountRecord_EVERYBODY {
sig, err := s.Client.Store.PNIIdentityKeyPair.SignAlternateIdentity(s.Client.Store.ACIIdentityKeyPair.GetIdentityKey())
if err != nil {
return fmt.Errorf("failed to generate PNI signature: %w", err)
}
pniSig = &signalpb.PniSignatureMessage{
Pni: s.Client.Store.PNI[:],
Signature: sig,
}
}
res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
Content: &signalpb.Content_DataMessage{DataMessage: &signalpb.DataMessage{
Flags: proto.Uint32(uint32(signalpb.DataMessage_PROFILE_KEY_UPDATE)),
ProfileKey: profileKey.Slice(),
Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)),
RequiredProtocolVersion: proto.Uint32(0),
}},
PniSignatureMessage: pniSig,
})
if !res.WasSuccessful {
return fmt.Errorf("failed to share profile key to accept message request: %w", res.Error)
}
// TODO send read receipts too?
}
return nil
}

View file

@ -20,15 +20,12 @@ import (
"context"
"encoding/base64"
"fmt"
"slices"
"strings"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/exzerolog"
"go.mau.fi/util/jsontime"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
@ -38,7 +35,6 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
@ -56,8 +52,6 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
return s.handleSignalReadSelf(evt)
case *events.DeleteForMe:
return s.handleSignalDeleteForMe(evt)
case *events.MessageRequestResponse:
return s.handleSignalMessageRequestResponse(evt)
case *events.Call:
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
case *events.ContactList:
@ -104,9 +98,6 @@ func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridg
if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
content.MsgType = event.MsgText
}
content.BeeperActionMessage = &event.BeeperActionMessage{
Type: event.BeeperActionMessageCall,
}
} else {
content.Body = "Call ended"
}
@ -176,7 +167,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
case *signalpb.DataMessage:
switch {
case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil,
innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil,
innerEvt.Payment != nil, innerEvt.GiftBadge != nil,
innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
return bridgev2.RemoteEventMessage
@ -185,7 +176,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
return bridgev2.RemoteEventReactionRemove
}
return bridgev2.RemoteEventReaction
case innerEvt.Delete != nil, innerEvt.AdminDelete != nil:
case innerEvt.Delete != nil:
return bridgev2.RemoteEventMessageRemove
case innerEvt.GetGroupV2().GetGroupChange() != nil:
return bridgev2.RemoteEventChatInfoChange
@ -294,21 +285,16 @@ func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
}
func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
var targetAuthorACI uuid.UUID
var targetAuthorACI string
var targetSentTS uint64
switch innerEvt := evt.Event.(type) {
case *signalpb.DataMessage:
switch {
case innerEvt.Reaction != nil:
targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary())
targetAuthorACI = innerEvt.Reaction.GetTargetAuthorAci()
targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
case innerEvt.Delete != nil:
targetSentTS = innerEvt.Delete.GetTargetSentTimestamp()
case innerEvt.AdminDelete != nil:
if len(innerEvt.AdminDelete.GetTargetAuthorAciBinary()) == 16 {
targetAuthorACI = uuid.UUID(innerEvt.AdminDelete.GetTargetAuthorAciBinary())
}
targetSentTS = innerEvt.AdminDelete.GetTargetSentTimestamp()
default:
return ""
}
@ -317,10 +303,11 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
default:
return ""
}
if targetAuthorACI == uuid.Nil {
targetAuthorACI = evt.Info.Sender
targetAuthorUUID := evt.Info.Sender
if targetAuthorACI != "" {
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
}
return signalid.MakeMessageID(targetAuthorACI, targetSentTS)
return signalid.MakeMessageID(targetAuthorUUID, targetSentTS)
}
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
@ -340,7 +327,7 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
if !ok {
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
}
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, dataMsg, nil)
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, dataMsg, nil)
if converted.Disappear.Type != "" {
evtTS := evt.GetTimestamp()
if !dataMsg.GetIsViewOnce() {
@ -359,52 +346,20 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
return converted, nil
}
const editStubPartID networkid.PartID = "editstub"
func isEditStub(msg *database.Message) bool {
return msg.PartID == editStubPartID
}
// editStubMessage returns a non-bridged message part which is saved as a placeholder row pointing
// at the pre-edit ID of a message, such that duplicate checks on incoming edits find it and are
// dropped. This is necessary because the first time we see an edit it modifies the ID in place.
func editStubMessage() *bridgev2.ConvertedMessage {
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
ID: editStubPartID,
Type: event.EventMessage,
Content: &event.MessageEventContent{},
DontBridge: true,
}},
}
}
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
editMsg, ok := evt.Event.(*signalpb.EditMessage)
if !ok {
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
}
existing = slices.DeleteFunc(slices.Clone(existing), isEditStub)
if len(existing) == 0 {
return nil, fmt.Errorf("%w: edit target has already been edited", bridgev2.ErrIgnoringRemoteEvent)
}
// TODO tell converter about existing parts to avoid reupload?
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, editMsg.GetDataMessage(), nil)
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, editMsg.GetDataMessage(), nil)
// TODO can anything other than the text be edited?
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
prevID := editPart.Part.ID
// Clone the database message struct to avoid mutating the ID.
// The ID from the original struct is used for AddedParts (we specifically want the old ID for that)
editPart.Part = ptr.Clone(editPart.Part)
editPart.Part.EditCount++
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
convertedEdit := &bridgev2.ConvertedEdit{
return &bridgev2.ConvertedEdit{
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
}
if prevID != editPart.Part.ID {
convertedEdit.AddedParts = editStubMessage()
}
return convertedEdit, nil
}, nil
}
func (evt *Bv2ChatEvent) GetStreamOrder() int64 {
@ -459,7 +414,7 @@ func (b *Bv2Receipt) GetReadUpTo() time.Time {
return time.Time{}
}
var _ bridgev2.RemoteReadReceipt = (*Bv2Receipt)(nil)
var _ bridgev2.RemoteReceipt = (*Bv2Receipt)(nil)
func convertReceipts[T any](ctx context.Context, input []T, getMessageFunc func(ctx context.Context, msgID T) (*database.Message, error)) map[networkid.PortalKey]*Bv2Receipt {
log := zerolog.Ctx(ctx)
@ -505,7 +460,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
Stringer("sender_id", evt.Sender).
Stringer("receipt_type", evt.Content.GetType()).
Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := log.WithContext(context.TODO())
receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS))
})
@ -516,9 +471,9 @@ func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) bool {
log := s.UserLogin.Log.With().
Str("action", "handle signal read self").
Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := log.WithContext(context.TODO())
receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) {
aciUUID, err := signalmeow.ParseStringOrBinaryUUID(msgInfo.GetSenderAci(), msgInfo.GetSenderAciBinary())
aciUUID, err := uuid.Parse(msgInfo.GetSenderAci())
if err != nil {
return nil, err
}
@ -537,13 +492,6 @@ func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signa
return networkid.PortalKey{}, false
}
return s.makeDMPortalKey(serviceID), true
case *signalpb.ConversationIdentifier_ThreadServiceIdBinary:
serviceID, err := libsignalgo.ServiceIDFromBytes(ident.ThreadServiceIdBinary)
if err != nil {
log.Err(err).Hex("chat_id", ident.ThreadServiceIdBinary).Msg("Failed to parse delete for me conversation ID")
return networkid.PortalKey{}, false
}
return s.makeDMPortalKey(serviceID), true
case *signalpb.ConversationIdentifier_ThreadGroupId:
if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
log.Error().
@ -582,22 +530,6 @@ func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey net
return ""
}
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
case *signalpb.AddressableMessage_AuthorServiceIdBinary:
serviceID, err := libsignalgo.ServiceIDFromBytes(typedAuthor.AuthorServiceIdBinary)
if err != nil {
log.Err(err).
Object("portal_key", portalKey).
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
Msg("Failed to parse delete for me message author service ID")
return ""
} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().
Object("portal_key", portalKey).
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
Msg("Dropping delete for me message with unsupported service ID type")
return ""
}
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
case *signalpb.AddressableMessage_AuthorE164:
log.Warn().
Object("portal_key", portalKey).
@ -688,45 +620,13 @@ func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
return true
}
func (s *SignalClient) handleSignalMessageRequestResponse(evt *events.MessageRequestResponse) bool {
if evt.Type != signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
// TODO do we need to do anything with blocks/deletes here or are they sent as normal delete events?
return true
}
var portalKey networkid.PortalKey
if evt.GroupID != nil {
portalKey = s.makePortalKey(evt.GroupID.String())
} else if evt.ThreadACI != uuid.Nil {
portalKey = s.makeDMPortalKey(libsignalgo.NewACIServiceID(evt.ThreadACI))
} else {
return true
}
res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
PortalKey: portalKey,
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
StreamOrder: int64(evt.Timestamp),
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("action", "unmark message request").Str("source", "sync message")
},
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &bridgev2.ChatInfo{
MessageRequest: ptr.Ptr(false),
},
},
})
return res.Success
}
func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
log := s.UserLogin.Log.With().
Str("action", "handle aci found").
Stringer("aci", evt.ACI).
Stringer("pni", evt.PNI).
Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := log.WithContext(context.TODO())
pniPortalKey := s.makeDMPortalKey(evt.PNI)
aciPortalKey := s.makeDMPortalKey(evt.ACI)
result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey)
@ -746,7 +646,7 @@ func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := log.WithContext(context.TODO())
for _, contact := range evt.Contacts {
if contact.ACI == uuid.Nil {
continue
@ -774,33 +674,6 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
if contact.ACI == s.Client.Store.ACI {
s.updateRemoteProfile(ctx, true)
}
if ptr.Val(contact.Whitelisted) {
portal, err := s.Main.Bridge.GetExistingPortalByKey(ctx, s.makeDMPortalKey(libsignalgo.NewACIServiceID(contact.ACI)))
if err != nil {
log.Err(err).Msg("Failed to get existing portal to update contact info")
continue
} else if portal != nil && portal.MessageRequest {
s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("action", "unmark message request").Str("source", "contact list")
},
PortalKey: portal.PortalKey,
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &bridgev2.ChatInfo{
MessageRequest: ptr.Ptr(false),
},
},
})
}
}
}
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).LastContactSync = jsontime.UnixMilliNow()
err := s.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to update last contact sync time")
}
}

View file

@ -18,12 +18,9 @@ package connector
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/coder/websocket"
"github.com/google/uuid"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
@ -44,7 +41,7 @@ func (s *SignalConnector) GetLoginFlows() []bridgev2.LoginFlow {
func (s *SignalConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
if flowID != "qr" {
return nil, bridgev2.ErrInvalidLoginFlowID
return nil, fmt.Errorf("invalid login flow ID")
}
return &QRLogin{User: user, Main: s}, nil
}
@ -55,6 +52,8 @@ type QRLogin struct {
cancelChan context.CancelFunc
ProvChan chan signalmeow.ProvisioningResponse
newQRCount int
ProvData *store.DeviceData
}
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
@ -69,84 +68,16 @@ func (qr *QRLogin) Cancel() {
const (
LoginStepQR = "fi.mau.signal.login.qr"
LoginStepProcess = "fi.mau.signal.login.processing"
LoginStepComplete = "fi.mau.signal.login.complete"
)
const (
qrRefreshInterval = 45 * time.Second
maxQRRefreshes = 20
)
var (
ErrLoginTimedOut = bridgev2.RespError{
ErrCode: "FI.MAU.BRIDGE.LOGIN_TIMED_OUT",
Err: "The QR code wasn't scanned in time, please start a new login",
StatusCode: http.StatusGone,
}
ErrLoginCancelled = bridgev2.RespError{
ErrCode: "FI.MAU.BRIDGE.LOGIN_CANCELLED",
Err: "Login process was cancelled",
StatusCode: http.StatusGone,
}
ErrDeviceLinkMissingCapability = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_MISSING_CAPABILITY",
Err: "Signal rejected linking because the bridge is missing a capability required by your account's other devices. Please try again later",
StatusCode: http.StatusConflict,
}
ErrDeviceLimitReached = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LIMIT_REACHED",
Err: "Your Signal account already has the maximum number of linked devices. Remove one in the Signal app and try again",
StatusCode: http.StatusBadRequest,
}
ErrDeviceLinkCodeInvalid = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_CODE_INVALID",
Err: "The scanned QR code was invalid or already used, please start a new login",
StatusCode: http.StatusForbidden,
}
ErrDeviceLinkRateLimited = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED",
Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again",
StatusCode: http.StatusTooManyRequests,
}
ErrDeviceLinkRejected = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED",
Err: "Signal rejected linking the device",
StatusCode: http.StatusBadRequest,
}
)
// Statuses of PUT /v1/devices/link, per Signal-Server's DeviceController
func wrapProvisioningError(err error) error {
var linkErr signalmeow.DeviceLinkError
if errors.As(err, &linkErr) {
switch linkErr.StatusCode {
case http.StatusConflict:
return ErrDeviceLinkMissingCapability
case http.StatusLengthRequired:
return ErrDeviceLimitReached
case http.StatusForbidden:
return ErrDeviceLinkCodeInvalid
case http.StatusTooManyRequests:
return ErrDeviceLinkRateLimited
default:
if linkErr.Message != "" {
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d: %s)", linkErr.StatusCode, linkErr.Message)
}
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d)", linkErr.StatusCode)
}
}
if websocket.CloseStatus(err) == websocket.StatusGoingAway {
return ErrLoginTimedOut
}
return err
}
func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
log := qr.Main.Bridge.Log.With().
Str("action", "login").
Stringer("user_id", qr.User.MXID).
Logger()
provCtx, cancel := context.WithCancel(log.WithContext(qr.Main.Bridge.BackgroundCtx))
provCtx, cancel := context.WithCancel(log.WithContext(context.Background()))
qr.cancelChan = cancel
// Don't use the start context here: the channel will outlive the start request.
qr.ProvChan = signalmeow.PerformProvisioning(
@ -156,16 +87,14 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
select {
case resp = <-qr.ProvChan:
if resp.Err != nil {
return nil, wrapProvisioningError(resp.Err)
return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningURLReceived {
return nil, fmt.Errorf("unexpected state %v", resp.State)
}
case <-ctx.Done():
cancel()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, ErrLoginTimedOut
}
return nil, ErrLoginCancelled
return nil, ctx.Err()
// TODO separate timeout here?
}
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait,
@ -183,11 +112,19 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
return nil, fmt.Errorf("login not started")
}
if qr.ProvData == nil {
return qr.qrWait(ctx)
} else {
return qr.processingWait(ctx)
}
}
func (qr *QRLogin) qrWait(ctx context.Context) (*bridgev2.LoginStep, error) {
select {
case resp := <-qr.ProvChan:
if resp.Err != nil {
qr.cancelChan()
return nil, wrapProvisioningError(resp.Err)
return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningDataReceived {
qr.cancelChan()
return nil, fmt.Errorf("unexpected state %v", resp.State)
@ -195,34 +132,52 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
qr.cancelChan()
return nil, fmt.Errorf("no signal account ID received")
}
return qr.loginComplete(ctx, resp.ProvisioningData)
qr.ProvData = resp.ProvisioningData
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait,
StepID: LoginStepProcess,
Instructions: fmt.Sprintf("Processing login as %s...", resp.ProvisioningData.Number),
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
Type: bridgev2.LoginDisplayTypeNothing,
},
}, nil
// Server will timeout the request after 60 seconds, but Signal Desktop opens
// a new socket and gets a new QR code after 45 seconds. We should do the same.
case <-time.After(qrRefreshInterval):
case <-time.After(45 * time.Second):
qr.cancelChan()
qr.newQRCount++
if qr.newQRCount >= maxQRRefreshes {
return nil, ErrLoginTimedOut
if qr.newQRCount >= 6 {
return nil, fmt.Errorf("too many QR code refreshes")
}
return qr.Start(ctx)
case <-ctx.Done():
qr.cancelChan()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, ErrLoginTimedOut
}
return nil, ErrLoginCancelled
return nil, ctx.Err()
}
}
func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData) (*bridgev2.LoginStep, error) {
func (qr *QRLogin) processingWait(ctx context.Context) (*bridgev2.LoginStep, error) {
defer qr.cancelChan()
newLoginID := signalid.MakeUserLoginID(qr.ProvData.ACI)
select {
case resp := <-qr.ProvChan:
if resp.Err != nil {
return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningPreKeysRegistered {
return nil, fmt.Errorf("unexpected state %v", resp.State)
}
case <-ctx.Done():
return nil, ctx.Err()
}
ul, err := qr.User.NewLogin(ctx, &database.UserLogin{
ID: signalid.MakeUserLoginID(provData.ACI),
RemoteName: provData.Number,
ID: newLoginID,
RemoteName: qr.ProvData.Number,
RemoteProfile: status.RemoteProfile{
Phone: provData.Number,
Phone: qr.ProvData.Number,
},
Metadata: &signalid.UserLoginMetadata{},
}, &bridgev2.NewLoginParams{
@ -235,7 +190,7 @@ func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeComplete,
StepID: LoginStepComplete,
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", provData.Number, provData.ACI),
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", qr.ProvData.Number, qr.ProvData.ACI),
CompleteParams: &bridgev2.LoginCompleteParams{
UserLoginID: ul.ID,
UserLogin: ul,

View file

@ -20,18 +20,18 @@ package libsignalgo
#include "./libsignal-ffi.h"
*/
import "C"
import "runtime"
import (
"runtime"
"unsafe"
)
type AccountEntropyPool string
type SVRKey = fixedArray32
func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
var out SVRKey
aepC, free := GoStringToCString(string(aep))
defer free()
var out [C.SignalSVR_KEY_LEN]byte
signalFfiError := C.signal_account_entropy_pool_derive_svr_key(
out.cFixedArray(),
aepC,
(*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
C.CString(string(aep)),
)
runtime.KeepAlive(aep)
if signalFfiError != nil {
@ -41,12 +41,10 @@ func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
}
func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) {
var out BackupKey
aepC, free := GoStringToCString(string(aep))
defer free()
var out [C.SignalBACKUP_KEY_LEN]byte
signalFfiError := C.signal_account_entropy_pool_derive_backup_key(
out.cFixedArray(),
aepC,
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
C.CString(string(aep)),
)
runtime.KeepAlive(aep)
if signalFfiError != nil {

View file

@ -46,9 +46,7 @@ func NewUUIDAddressFromString(uuidStr string, deviceID uint) (*Address, error) {
func newAddress(name string, deviceID uint) (*Address, error) {
var pa C.SignalMutPointerProtocolAddress
nameStr, freeNameStr := GoStringToCString(name)
defer freeNameStr()
signalFfiError := C.signal_address_new(&pa, nameStr, C.uint(deviceID))
signalFfiError := C.signal_address_new(&pa, C.CString(name), C.uint(deviceID))
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
@ -83,7 +81,7 @@ func (pa *Address) CancelFinalizer() {
}
func (pa *Address) Name() (string, error) {
var name C.SignalCStringPtr
var name *C.char
signalFfiError := C.signal_address_get_name(&name, pa.constPtr())
runtime.KeepAlive(pa)
if signalFfiError != nil {

View file

@ -24,16 +24,15 @@ package libsignalgo
import "C"
import (
"fmt"
"unsafe"
"github.com/google/uuid"
)
// type AuthCredential [181]byte
// type AuthCredentialResponse [361]byte
const AuthCredentialWithPniLength = 265
type AuthCredentialWithPni [AuthCredentialWithPniLength]byte
type AuthCredentialWithPniResponse [425]byte
// type AuthCredential [C.SignalAUTH_CREDENTIAL_LEN]byte
// type AuthCredentialResponse [C.SignalAUTH_CREDENTIAL_RESPONSE_LEN]byte
type AuthCredentialWithPni [C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN]byte
type AuthCredentialWithPniResponse [C.SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]byte
type AuthCredentialPresentation []byte
func (ac *AuthCredentialWithPni) Slice() []byte {
@ -52,8 +51,8 @@ func ReceiveAuthCredentialWithPni(
signalFfiError := C.signal_server_public_params_receive_auth_credential_with_pni_as_service_id(
&c_result,
C.SignalConstPointerServerPublicParams{serverPublicParams},
NewACIServiceID(aci).cConstFixedArray(),
NewPNIServiceID(pni).cConstFixedArray(),
NewACIServiceID(aci).CFixedBytes(),
NewPNIServiceID(pni).CFixedBytes(),
C.uint64_t(redemptionTime),
BytesToBuffer(authCredResponse[:]),
)
@ -61,8 +60,8 @@ func ReceiveAuthCredentialWithPni(
return nil, wrapError(signalFfiError)
}
resultBytes := CopySignalOwnedBufferToBytes(c_result)
if len(resultBytes) != AuthCredentialWithPniLength {
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), AuthCredentialWithPniLength)
if len(resultBytes) != C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN {
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN)
}
return (*AuthCredentialWithPni)(resultBytes), nil
}
@ -84,12 +83,14 @@ func CreateAuthCredentialWithPniPresentation(
authCredWithPni AuthCredentialWithPni,
) (*AuthCredentialPresentation, error) {
var c_result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
c_randomness := (*[C.SignalRANDOMNESS_LEN]C.uchar)(unsafe.Pointer(&randomness[0]))
c_groupSecretParams := (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(&groupSecretParams[0]))
signalFfiError := C.signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic(
&c_result,
C.SignalConstPointerServerPublicParams{serverPublicParams},
randomness.cConstFixedArray(),
groupSecretParams.cConstFixedArray(),
c_randomness,
c_groupSecretParams,
BytesToBuffer(authCredWithPni[:]),
)
if signalFfiError != nil {

View file

@ -27,9 +27,7 @@ import (
"go.mau.fi/util/random"
)
const BackupKeyLength = 32
type BackupKey [BackupKeyLength]byte
type BackupKey [C.SignalBACKUP_KEY_LEN]byte
func (bk *BackupKey) Slice() []byte {
if bk == nil {
@ -40,25 +38,17 @@ func (bk *BackupKey) Slice() []byte {
const BackupIDLength = 16
type BackupID = fixedArray16
type BackupMetadataKey = fixedArray32
type BackupMediaID = fixedArray15
type BackupMediaKey = fixedArray64
func (bk *BackupKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk))
}
func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(bk.cFixedArray())
}
type BackupID [BackupIDLength]byte
type BackupMetadataKey [C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]byte
type BackupMediaID [C.SignalMEDIA_ID_LEN]byte
type BackupMediaKey [C.SignalMEDIA_ENCRYPTION_KEY_LEN]byte
func GenerateRandomBackupKey() *BackupKey {
return (*BackupKey)(random.Bytes(BackupKeyLength))
return (*BackupKey)(random.Bytes(C.SignalBACKUP_KEY_LEN))
}
func BytesToBackupKey(bytes []byte) *BackupKey {
if len(bytes) != BackupKeyLength {
if len(bytes) != C.SignalBACKUP_KEY_LEN {
return nil
}
return (*BackupKey)(bytes)
@ -67,9 +57,9 @@ func BytesToBackupKey(bytes []byte) *BackupKey {
func (bk *BackupKey) DeriveBackupID(aci ServiceID) (*BackupID, error) {
var out BackupID
signalFfiError := C.signal_backup_key_derive_backup_id(
out.cFixedArray(),
bk.cConstFixedArray(),
aci.cConstFixedArray(),
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
aci.CFixedBytes(),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -82,8 +72,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
var out C.SignalMutPointerPrivateKey
signalFfiError := C.signal_backup_key_derive_ec_key(
&out,
bk.cConstFixedArray(),
aci.cConstFixedArray(),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&bk)),
aci.CFixedBytes(),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -95,8 +85,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error) {
var out BackupMetadataKey
signalFfiError := C.signal_backup_key_derive_local_backup_metadata_key(
out.cFixedArray(),
bk.cConstFixedArray(),
(*[C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -107,12 +97,10 @@ func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error)
func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
var out BackupMediaID
mediaNameStr, mediaNameFree := GoStringToCString(mediaName)
defer mediaNameFree()
signalFfiError := C.signal_backup_key_derive_media_id(
out.cFixedArray(),
bk.cConstFixedArray(),
mediaNameStr,
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
C.CString(mediaName),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -124,9 +112,9 @@ func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
var out BackupMediaKey
signalFfiError := C.signal_backup_key_derive_media_encryption_key(
out.cFixedArray(),
bk.cConstFixedArray(),
mediaID.cConstFixedArray(),
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
)
runtime.KeepAlive(bk)
runtime.KeepAlive(mediaID)
@ -139,9 +127,9 @@ func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMe
func (bk *BackupKey) DeriveThumbnailTransitEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
var out BackupMediaKey
signalFfiError := C.signal_backup_key_derive_thumbnail_transit_encryption_key(
out.cFixedArray(),
bk.cConstFixedArray(),
mediaID.cConstFixedArray(),
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
)
runtime.KeepAlive(bk)
runtime.KeepAlive(mediaID)

View file

@ -21,8 +21,6 @@ package libsignalgo
*/
import "C"
import (
"fmt"
"runtime"
"unsafe"
)
@ -44,22 +42,6 @@ func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
return buf
}
func ManyBytesToBuffer[T ~[]byte](datas []T) (C.SignalBorrowedSliceOfBuffers, func()) {
buffers := make([]C.SignalBorrowedBuffer, len(datas))
var pinner runtime.Pinner
for i, data := range datas {
if len(data) == 0 {
panic(fmt.Errorf("empty slice passed to ManyBytesToBuffer at index %d", i))
}
pinner.Pin(&data[0])
buffers[i] = BytesToBuffer(data)
}
return C.SignalBorrowedSliceOfBuffers{
base: unsafe.SliceData(buffers),
length: C.size_t(len(buffers)),
}, pinner.Unpin
}
func EmptyBorrowedBuffer() C.SignalBorrowedBuffer {
return C.SignalBorrowedBuffer{}
}

View file

@ -1,6 +1,6 @@
package libsignalgo
/*
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz -lstdc++
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz
*/
import "C"

View file

@ -18,20 +18,12 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
import "unsafe"
func GoStringToCString(str string) (C.SignalCStringPtr, func()) {
cStr := C.CString(str)
return C.SignalCStringPtr(unsafe.Pointer(cStr)), func() {
C.free(unsafe.Pointer(cStr))
}
}
func CopyCStringToString(cString C.SignalCStringPtr) (s string) {
s = C.GoString((*C.char)(unsafe.Pointer(cString)))
func CopyCStringToString(cString *C.char) (s string) {
s = C.GoString(cString)
C.signal_free_string(cString)
return
}
@ -47,17 +39,3 @@ func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
C.signal_free_buffer(buffer.base, buffer.length)
return
}
func CopySignalBytestringArray[T ~[]byte](buffer C.SignalBytestringArray) (b []T) {
concatted := C.GoBytes(unsafe.Pointer(buffer.bytes.base), C.int(buffer.bytes.length))
b = make([]T, int(buffer.lengths.length))
sizeTSize := unsafe.Sizeof(C.size_t(0))
offset := 0
for i := 0; i < int(buffer.lengths.length); i++ {
length := int(*(*C.size_t)(unsafe.Add(unsafe.Pointer(buffer.lengths.base), uintptr(i)*sizeTSize)))
b[i] = concatted[offset : offset+length]
offset += length
}
C.signal_free_bytestring_array(buffer)
return
}

View file

@ -23,6 +23,7 @@ package libsignalgo
import "C"
import (
"runtime"
"time"
)
type DecryptionErrorMessage struct {
@ -48,7 +49,7 @@ func DeserializeDecryptionErrorMessage(messageBytes []byte) (*DecryptionErrorMes
return wrapDecryptionErrorMessage(dem.raw), nil
}
func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType CiphertextMessageType, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) {
func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType uint8, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) {
var dem C.SignalMutPointerDecryptionErrorMessage
signalFfiError := C.signal_decryption_error_message_for_original_message(
&dem,
@ -111,14 +112,14 @@ func (dem *DecryptionErrorMessage) Serialize() ([]byte, error) {
return CopySignalOwnedBufferToBytes(serialized), nil
}
func (dem *DecryptionErrorMessage) GetTimestamp() (uint64, error) {
func (dem *DecryptionErrorMessage) GetTimestamp() (time.Time, error) {
var ts C.uint64_t
signalFfiError := C.signal_decryption_error_message_get_timestamp(&ts, dem.constPtr())
runtime.KeepAlive(dem)
if signalFfiError != nil {
return 0, wrapError(signalFfiError)
return time.Time{}, wrapError(signalFfiError)
}
return uint64(ts), nil
return time.UnixMilli(int64(ts)), nil
}
func (dem *DecryptionErrorMessage) GetDeviceID() (uint32, error) {

View file

@ -43,9 +43,7 @@ func (dtk *DeviceTransferKey) PrivateKeyMaterial() []byte {
func (dtk *DeviceTransferKey) GenerateCertificate(name string, days int) ([]byte, error) {
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
nameStr, freeNameStr := GoStringToCString(name)
defer freeNameStr()
signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), nameStr, C.uint32_t(days))
signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), C.CString(name), C.uint32_t(days))
runtime.KeepAlive(dtk)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)

View file

@ -26,10 +26,6 @@ import (
type ErrorCode int
func (e ErrorCode) Error() string {
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
}
const (
ErrorCodeUnknownError ErrorCode = 1
ErrorCodeInvalidState ErrorCode = 2
@ -38,7 +34,6 @@ const (
ErrorCodeInvalidArgument ErrorCode = 5
ErrorCodeInvalidType ErrorCode = 6
ErrorCodeInvalidUtf8String ErrorCode = 7
ErrorCodeCancelled ErrorCode = 8
ErrorCodeProtobufError ErrorCode = 10
ErrorCodeLegacyCiphertextVersion ErrorCode = 21
ErrorCodeUnknownCiphertextVersion ErrorCode = 22
@ -56,61 +51,9 @@ const (
ErrorCodeInvalidRegistrationId ErrorCode = 81
ErrorCodeInvalidSession ErrorCode = 82
ErrorCodeInvalidSenderKeySession ErrorCode = 83
ErrorCodeInvalidProtocolAddress ErrorCode = 84
ErrorCodeDuplicatedMessage ErrorCode = 90
ErrorCodeCallbackError ErrorCode = 100
ErrorCodeVerificationFailure ErrorCode = 110
ErrorCodeUsernameCannotBeEmpty ErrorCode = 120
ErrorCodeUsernameCannotStartWithDigit ErrorCode = 121
ErrorCodeUsernameMissingSeparator ErrorCode = 122
ErrorCodeUsernameBadDiscriminatorCharacter ErrorCode = 123
ErrorCodeUsernameBadNicknameCharacter ErrorCode = 124
ErrorCodeUsernameTooShort ErrorCode = 125
ErrorCodeUsernameTooLong ErrorCode = 126
ErrorCodeUsernameLinkInvalidEntropyDataLength ErrorCode = 127
ErrorCodeUsernameLinkInvalid ErrorCode = 128
ErrorCodeUsernameDiscriminatorCannotBeEmpty ErrorCode = 130
ErrorCodeUsernameDiscriminatorCannotBeZero ErrorCode = 131
ErrorCodeUsernameDiscriminatorCannotBeSingleDigit ErrorCode = 132
ErrorCodeUsernameDiscriminatorCannotHaveLeadingZeros ErrorCode = 133
ErrorCodeUsernameDiscriminatorTooLarge ErrorCode = 134
ErrorCodeIoError ErrorCode = 140
ErrorCodeInvalidMediaInput ErrorCode = 141
ErrorCodeUnsupportedMediaInput ErrorCode = 142
ErrorCodeConnectionTimedOut ErrorCode = 143
ErrorCodeNetworkProtocol ErrorCode = 144
ErrorCodeRateLimited ErrorCode = 145
ErrorCodeWebSocket ErrorCode = 146
ErrorCodeCdsiInvalidToken ErrorCode = 147
ErrorCodeConnectionFailed ErrorCode = 148
ErrorCodeChatServiceInactive ErrorCode = 149
ErrorCodeRequestTimedOut ErrorCode = 150
ErrorCodeRateLimitChallenge ErrorCode = 151
ErrorCodePossibleCaptiveNetwork ErrorCode = 152
ErrorCodeSvrDataMissing ErrorCode = 160
ErrorCodeSvrRestoreFailed ErrorCode = 161
ErrorCodeSvrRotationMachineTooManySteps ErrorCode = 162
ErrorCodeSvrRequestFailed ErrorCode = 163
ErrorCodeAppExpired ErrorCode = 170
ErrorCodeDeviceDeregistered ErrorCode = 171
ErrorCodeConnectionInvalidated ErrorCode = 172
ErrorCodeConnectedElsewhere ErrorCode = 173
ErrorCodeBackupValidation ErrorCode = 180
ErrorCodeRegistrationInvalidSessionId ErrorCode = 190
ErrorCodeRegistrationUnknown ErrorCode = 192
ErrorCodeRegistrationSessionNotFound ErrorCode = 193
ErrorCodeRegistrationNotReadyForVerification ErrorCode = 194
ErrorCodeRegistrationSendVerificationCodeFailed ErrorCode = 195
ErrorCodeRegistrationCodeNotDeliverable ErrorCode = 196
ErrorCodeRegistrationSessionUpdateRejected ErrorCode = 197
ErrorCodeRegistrationCredentialsCouldNotBeParsed ErrorCode = 198
ErrorCodeRegistrationDeviceTransferPossible ErrorCode = 199
ErrorCodeRegistrationRecoveryVerificationFailed ErrorCode = 200
ErrorCodeRegistrationLock ErrorCode = 201
ErrorCodeKeyTransparencyError ErrorCode = 210
ErrorCodeKeyTransparencyVerificationFailed ErrorCode = 211
ErrorCodeRequestUnauthorized ErrorCode = 220
ErrorCodeMismatchedDevices ErrorCode = 221
)
type SignalError struct {
@ -122,10 +65,6 @@ func (e *SignalError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
func (e *SignalError) Unwrap() error {
return e.Code
}
func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error {
if signalError == nil {
return nil
@ -152,7 +91,7 @@ func wrapError(signalError *C.SignalFfiError) error {
}
func wrapSignalError(signalError *C.SignalFfiError, errorType C.uint32_t) error {
var messageBytes C.SignalCStringPtr
var messageBytes *C.char
getMessageError := C.signal_error_get_message(&messageBytes, signalError)
if getMessageError != nil {
// Ignore any errors from this, it will just end up being an empty string.

View file

@ -92,7 +92,7 @@ func (f *Fingerprint) ScannableEncoding() ([]byte, error) {
}
func (f *Fingerprint) DisplayString() (string, error) {
var displayString C.SignalCStringPtr
var displayString *C.char
signalFfiError := C.signal_fingerprint_display_string(&displayString, f.constPtr())
runtime.KeepAlive(f)
if signalFfiError != nil {

View file

@ -1,159 +0,0 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
import "unsafe"
type fixedArray15 [15]byte
type fixedArray16 [16]byte
type fixedArray17 [17]byte
type fixedArray32 [32]byte
type fixedArray64 [64]byte
type fixedArray65 [65]byte
type fixedArray97 [97]byte
type fixedArray129 [129]byte
type fixedArray153 [153]byte
type fixedArray177 [177]byte
type fixedArray289 [289]byte
type fixedArray329 [329]byte
type fixedArray409 [409]byte
type fixedArray473 [473]byte
type fixedArray497 [497]byte
func (a *fixedArray15) cFixedArray() *C.SignalType_FixedArray15_uint8_t {
return (*C.SignalType_FixedArray15_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray15) cConstFixedArray() cFixedArray15Compat {
return cFixedArray15Compat(a.cFixedArray())
}
func (a *fixedArray16) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray16) cConstFixedArray() cFixedArray16Compat {
return cFixedArray16Compat(a.cFixedArray())
}
func (a *fixedArray17) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
return (*C.SignalType_FixedArray17_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray17) cConstFixedArray() cFixedArray17Compat {
return cFixedArray17Compat(a.cFixedArray())
}
func (a *fixedArray32) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray32) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(a.cFixedArray())
}
func (a *fixedArray64) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray64) cConstFixedArray() cFixedArray64Compat {
return cFixedArray64Compat(a.cFixedArray())
}
func (a *fixedArray65) cFixedArray() *C.SignalType_FixedArray65_uint8_t {
return (*C.SignalType_FixedArray65_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray65) cConstFixedArray() cFixedArray65Compat {
return cFixedArray65Compat(a.cFixedArray())
}
func (a *fixedArray97) cFixedArray() *C.SignalType_FixedArray97_uint8_t {
return (*C.SignalType_FixedArray97_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray97) cConstFixedArray() cFixedArray97Compat {
return cFixedArray97Compat(a.cFixedArray())
}
func (a *fixedArray129) cFixedArray() *C.SignalType_FixedArray129_uint8_t {
return (*C.SignalType_FixedArray129_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray129) cConstFixedArray() cFixedArray129Compat {
return cFixedArray129Compat(a.cFixedArray())
}
func (a *fixedArray153) cFixedArray() *C.SignalType_FixedArray153_uint8_t {
return (*C.SignalType_FixedArray153_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray153) cConstFixedArray() cFixedArray153Compat {
return cFixedArray153Compat(a.cFixedArray())
}
func (a *fixedArray177) cFixedArray() *C.SignalType_FixedArray177_uint8_t {
return (*C.SignalType_FixedArray177_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray177) cConstFixedArray() cFixedArray177Compat {
return cFixedArray177Compat(a.cFixedArray())
}
func (a *fixedArray289) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray289) cConstFixedArray() cFixedArray289Compat {
return cFixedArray289Compat(a.cFixedArray())
}
func (a *fixedArray329) cFixedArray() *C.SignalType_FixedArray329_uint8_t {
return (*C.SignalType_FixedArray329_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray329) cConstFixedArray() cFixedArray329Compat {
return cFixedArray329Compat(a.cFixedArray())
}
func (a *fixedArray409) cFixedArray() *C.SignalType_FixedArray409_uint8_t {
return (*C.SignalType_FixedArray409_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray409) cConstFixedArray() cFixedArray409Compat {
return cFixedArray409Compat(a.cFixedArray())
}
func (a *fixedArray473) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray473) cConstFixedArray() cFixedArray473Compat {
return cFixedArray473Compat(a.cFixedArray())
}
func (a *fixedArray497) cFixedArray() *C.SignalType_FixedArray497_uint8_t {
return (*C.SignalType_FixedArray497_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray497) cConstFixedArray() cFixedArray497Compat {
return cFixedArray497Compat(a.cFixedArray())
}

View file

@ -1,24 +0,0 @@
//go:build darwin || android || ios || (windows && arm64)
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
type cFixedArray15Compat = *C.SignalType_FixedArray15_uint8_t
type cFixedArray16Compat = *C.SignalType_FixedArray16_uint8_t
type cFixedArray17Compat = *C.SignalType_FixedArray17_uint8_t
type cFixedArray32Compat = *C.SignalType_FixedArray32_uint8_t
type cFixedArray64Compat = *C.SignalType_FixedArray64_uint8_t
type cFixedArray65Compat = *C.SignalType_FixedArray65_uint8_t
type cFixedArray97Compat = *C.SignalType_FixedArray97_uint8_t
type cFixedArray129Compat = *C.SignalType_FixedArray129_uint8_t
type cFixedArray153Compat = *C.SignalType_FixedArray153_uint8_t
type cFixedArray177Compat = *C.SignalType_FixedArray177_uint8_t
type cFixedArray289Compat = *C.SignalType_FixedArray289_uint8_t
type cFixedArray329Compat = *C.SignalType_FixedArray329_uint8_t
type cFixedArray409Compat = *C.SignalType_FixedArray409_uint8_t
type cFixedArray473Compat = *C.SignalType_FixedArray473_uint8_t
type cFixedArray497Compat = *C.SignalType_FixedArray497_uint8_t

View file

@ -1,26 +0,0 @@
//go:build !(darwin || android || ios || (windows && arm64))
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
// Hack for https://github.com/golang/go/issues/7270
// The clang version is more correct, but doesn't work with gcc.
type cFixedArray15Compat = *[15]C.uint8_t
type cFixedArray16Compat = *[16]C.uint8_t
type cFixedArray17Compat = *[17]C.uint8_t
type cFixedArray32Compat = *[32]C.uint8_t
type cFixedArray64Compat = *[64]C.uint8_t
type cFixedArray65Compat = *[65]C.uint8_t
type cFixedArray97Compat = *[97]C.uint8_t
type cFixedArray129Compat = *[129]C.uint8_t
type cFixedArray153Compat = *[153]C.uint8_t
type cFixedArray177Compat = *[177]C.uint8_t
type cFixedArray289Compat = *[289]C.uint8_t
type cFixedArray329Compat = *[329]C.uint8_t
type cFixedArray409Compat = *[409]C.uint8_t
type cFixedArray473Compat = *[473]C.uint8_t
type cFixedArray497Compat = *[497]C.uint8_t

View file

@ -36,7 +36,7 @@ func GroupEncrypt(ctx context.Context, ptext []byte, sender *Address, distributi
signalFfiError := C.signal_group_encrypt_message(
&ciphertextMessage,
sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)),
(*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
BytesToBuffer(ptext),
callbackCtx.wrapSenderKeyStore(store))
runtime.KeepAlive(ptext)

View file

@ -31,9 +31,7 @@ import (
"github.com/google/uuid"
)
const RandomnessLength = 32
type Randomness = fixedArray32
type Randomness [C.SignalRANDOMNESS_LEN]byte
func GenerateRandomness() Randomness {
var randomness Randomness
@ -44,39 +42,14 @@ func GenerateRandomness() Randomness {
return randomness
}
const GroupMasterKeyLength = 32
const GroupIdentifierLength = 32
const GroupSecretParamsLength = 289
const GroupMasterKeyLength = C.SignalGROUP_MASTER_KEY_LEN
const GroupIdentifierLength = C.SignalGROUP_IDENTIFIER_LEN
type GroupMasterKey [GroupMasterKeyLength]byte
type GroupSecretParams [GroupSecretParamsLength]byte
type GroupPublicParams = fixedArray97
type GroupSecretParams [C.SignalGROUP_SECRET_PARAMS_LEN]byte
type GroupPublicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]byte
type GroupIdentifier [GroupIdentifierLength]byte
func (gmk *GroupMasterKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gmk))
}
func (gmk *GroupMasterKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(gmk.cFixedArray())
}
func (gsp *GroupSecretParams) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(gsp))
}
func (gsp *GroupSecretParams) cConstFixedArray() cFixedArray289Compat {
return cFixedArray289Compat(gsp.cFixedArray())
}
func (gid *GroupIdentifier) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gid))
}
func (gid *GroupIdentifier) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(gid.cFixedArray())
}
func (gid *GroupIdentifier) String() string {
if gid == nil {
return ""
@ -84,8 +57,8 @@ func (gid *GroupIdentifier) String() string {
return base64.StdEncoding.EncodeToString(gid[:])
}
type UUIDCiphertext = fixedArray65
type ProfileKeyCiphertext = fixedArray65
type UUIDCiphertext [C.SignalUUID_CIPHERTEXT_LEN]byte
type ProfileKeyCiphertext [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]byte
func GenerateGroupSecretParams() (GroupSecretParams, error) {
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness())
@ -103,48 +76,52 @@ func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
}
}
func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
return DeriveGroupSecretParamsFromMasterKey(gmk)
}
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
var params GroupSecretParams
signalFfiError := C.signal_group_secret_params_generate_deterministic(params.cFixedArray(), randomness.cConstFixedArray())
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
runtime.KeepAlive(randomness)
if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError)
}
return params, nil
var groupSecretParams GroupSecretParams
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(&params), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
return groupSecretParams, nil
}
func DeriveGroupSecretParamsFromMasterKey(groupMasterKey GroupMasterKey) (GroupSecretParams, error) {
var params GroupSecretParams
signalFfiError := C.signal_group_secret_params_derive_from_master_key(params.cFixedArray(), groupMasterKey.cConstFixedArray())
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_derive_from_master_key(&params, (*[C.SignalGROUP_MASTER_KEY_LEN]C.uint8_t)(unsafe.Pointer(&groupMasterKey)))
runtime.KeepAlive(groupMasterKey)
if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError)
}
return params, nil
var groupSecretParams GroupSecretParams
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(&params), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
return groupSecretParams, nil
}
func (gsp *GroupSecretParams) GetPublicParams() (*GroupPublicParams, error) {
var publicParams GroupPublicParams
signalFfiError := C.signal_group_secret_params_get_public_params(publicParams.cFixedArray(), gsp.cConstFixedArray())
var publicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_get_public_params(&publicParams, (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)))
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &publicParams, nil
var groupPublicParams GroupPublicParams
copy(groupPublicParams[:], C.GoBytes(unsafe.Pointer(&publicParams), C.int(C.SignalGROUP_PUBLIC_PARAMS_LEN)))
return &groupPublicParams, nil
}
func GetGroupIdentifier(groupPublicParams GroupPublicParams) (*GroupIdentifier, error) {
var groupIdentifier GroupIdentifier
signalFfiError := C.signal_group_public_params_get_group_identifier(groupIdentifier.cFixedArray(), groupPublicParams.cConstFixedArray())
var groupIdentifier [C.SignalGROUP_IDENTIFIER_LEN]C.uchar
signalFfiError := C.signal_group_public_params_get_group_identifier(&groupIdentifier, (*[C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(&groupPublicParams)))
runtime.KeepAlive(groupPublicParams)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &groupIdentifier, nil
var result GroupIdentifier
copy(result[:], C.GoBytes(unsafe.Pointer(&groupIdentifier), C.int(C.SignalGROUP_IDENTIFIER_LEN)))
return &result, nil
}
func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error) {
@ -152,7 +129,7 @@ func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error
borrowedBlob := BytesToBuffer(blob)
signalFfiError := C.signal_group_secret_params_decrypt_blob_with_padding(
&plaintext,
gsp.cConstFixedArray(),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
borrowedBlob,
)
runtime.KeepAlive(gsp)
@ -168,8 +145,8 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
borrowedPlaintext := BytesToBuffer(plaintext)
signalFfiError := C.signal_group_secret_params_encrypt_blob_with_padding_deterministic(
&ciphertext,
gsp.cConstFixedArray(),
randomness.cConstFixedArray(),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
borrowedPlaintext,
(C.uint32_t)(padding_len),
)
@ -184,11 +161,11 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
}
func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
var serviceIDBytes ServiceIDFixedBytes
u := C.SignalServiceIdFixedWidthBinaryBytes{}
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
serviceIDBytes.cFixedArray(),
gsp.cConstFixedArray(),
ciphertextServiceID.cConstFixedArray(),
&u,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextServiceID)
@ -196,31 +173,33 @@ func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertex
return EmptyServiceID, wrapError(signalFfiError)
}
serviceID := ServiceIDFromCFixedBytes(serviceIDBytes.cFixedArray())
serviceID := ServiceIDFromCFixedBytes(&u)
return serviceID, nil
}
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) {
var cipherTextServiceID UUIDCiphertext
var cipherTextServiceID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
cipherTextServiceID.cFixedArray(),
gsp.cConstFixedArray(),
serviceID.cConstFixedArray(),
&cipherTextServiceID,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
serviceID.CFixedBytes(),
)
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &cipherTextServiceID, nil
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextServiceID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return &result, nil
}
func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyCiphertext, u uuid.UUID) (*ProfileKey, error) {
var profileKey ProfileKey
profileKey := [C.SignalPROFILE_KEY_LEN]C.uchar{}
signalFfiError := C.signal_group_secret_params_decrypt_profile_key(
profileKey.cFixedArray(),
gsp.cConstFixedArray(),
ciphertextProfileKey.cConstFixedArray(),
NewACIServiceID(u).cConstFixedArray(),
&profileKey,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextProfileKey)),
NewACIServiceID(u).CFixedBytes(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextProfileKey)
@ -228,23 +207,27 @@ func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyC
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &profileKey, nil
var result ProfileKey
copy(result[:], C.GoBytes(unsafe.Pointer(&profileKey), C.int(C.SignalPROFILE_KEY_LEN)))
return &result, nil
}
func (gsp *GroupSecretParams) EncryptProfileKey(profileKey ProfileKey, u uuid.UUID) (*ProfileKeyCiphertext, error) {
var ciphertextProfileKey ProfileKeyCiphertext
ciphertextProfileKey := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
signalFfiError := C.signal_group_secret_params_encrypt_profile_key(
ciphertextProfileKey.cFixedArray(),
gsp.cConstFixedArray(),
profileKey.cConstFixedArray(),
NewACIServiceID(u).cConstFixedArray(),
&ciphertextProfileKey,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalPROFILE_KEY_LEN]C.uint8_t)(unsafe.Pointer(&profileKey)),
NewACIServiceID(u).CFixedBytes(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(profileKey)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &ciphertextProfileKey, nil
var result ProfileKeyCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&ciphertextProfileKey), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
return &result, nil
}
func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp *ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
@ -253,9 +236,9 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
&out,
C.SignalConstPointerServerPublicParams{spp},
randomness.cConstFixedArray(),
gsp.cConstFixedArray(),
credential.cConstFixedArray(),
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar)(unsafe.Pointer(&credential)),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(credential)
@ -269,14 +252,16 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
}
func (gsp *GroupSecretParams) GetMasterKey() (*GroupMasterKey, error) {
var masterKey GroupMasterKey
masterKeyBytes := [C.SignalGROUP_MASTER_KEY_LEN]C.uchar{}
signalFfiError := C.signal_group_secret_params_get_master_key(
masterKey.cFixedArray(),
gsp.cConstFixedArray(),
&masterKeyBytes,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
)
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return &masterKey, nil
var groupMasterKey GroupMasterKey
copy(groupMasterKey[:], C.GoBytes(unsafe.Pointer(&masterKeyBytes), C.int(C.SignalGROUP_MASTER_KEY_LEN)))
return &groupMasterKey, nil
}

View file

@ -1,214 +0,0 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2025 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
import (
"encoding/base64"
"runtime"
"time"
)
type GroupSendFullToken []byte
func (gsft GroupSendFullToken) String() string {
return base64.StdEncoding.EncodeToString(gsft)
}
func (gsft GroupSendFullToken) CheckValidContents() error {
signalFfiError := C.signal_group_send_full_token_check_valid_contents(
BytesToBuffer(gsft),
)
runtime.KeepAlive(gsft)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gsft GroupSendFullToken) GetExpiration() (time.Time, error) {
var expiration C.uint64_t
signalFfiError := C.signal_group_send_full_token_get_expiration(
&expiration,
BytesToBuffer(gsft),
)
runtime.KeepAlive(gsft)
if signalFfiError != nil {
return time.Time{}, wrapError(signalFfiError)
}
return time.Unix(int64(expiration), 0), nil
}
type GroupSendToken []byte
func (gst GroupSendToken) CheckValidContents() error {
signalFfiError := C.signal_group_send_token_check_valid_contents(
BytesToBuffer(gst),
)
runtime.KeepAlive(gst)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gst GroupSendToken) ToFullToken(expiration time.Time) (GroupSendFullToken, error) {
var fullToken C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_token_to_full_token(
&fullToken,
BytesToBuffer(gst),
C.uint64_t(expiration.Unix()),
)
runtime.KeepAlive(gst)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(fullToken), nil
}
type GroupSendEndorsement []byte
func (gse GroupSendEndorsement) ToToken(groupSecretParams *GroupSecretParams) (GroupSendToken, error) {
var token C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_endorsement_to_token(
&token,
BytesToBuffer(gse),
groupSecretParams.cConstFixedArray(),
)
runtime.KeepAlive(gse)
runtime.KeepAlive(groupSecretParams)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(token), nil
}
func (gse GroupSendEndorsement) ToFullToken(params *GroupSecretParams, expiration time.Time) (GroupSendFullToken, error) {
token, err := gse.ToToken(params)
if err != nil {
return nil, err
}
return token.ToFullToken(expiration)
}
func (gse GroupSendEndorsement) CheckValidContents() error {
signalFfiError := C.signal_group_send_endorsement_check_valid_contents(
BytesToBuffer(gse),
)
runtime.KeepAlive(gse)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gse GroupSendEndorsement) Remove(other GroupSendEndorsement) (GroupSendEndorsement, error) {
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_endorsement_remove(
&result,
BytesToBuffer(gse),
BytesToBuffer(other),
)
runtime.KeepAlive(gse)
runtime.KeepAlive(other)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(result), nil
}
func GroupSendEndorsementCombine(endorsements ...GroupSendEndorsement) (GroupSendEndorsement, error) {
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
cEndorsements, unpin := ManyBytesToBuffer(endorsements)
defer unpin()
signalFfiError := C.signal_group_send_endorsement_combine(
&result,
cEndorsements,
)
runtime.KeepAlive(endorsements)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(result), nil
}
type GroupSendEndorsementsResponse []byte
func (gser GroupSendEndorsementsResponse) GetExpiration() (time.Time, error) {
var expiration C.uint64_t
signalFfiError := C.signal_group_send_endorsements_response_get_expiration(
&expiration,
BytesToBuffer(gser),
)
runtime.KeepAlive(gser)
if signalFfiError != nil {
return time.Time{}, wrapError(signalFfiError)
}
return time.Unix(int64(expiration), 0), nil
}
func (gser GroupSendEndorsementsResponse) CheckValidContents() error {
signalFfiError := C.signal_group_send_endorsements_response_check_valid_contents(
BytesToBuffer(gser),
)
runtime.KeepAlive(gser)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gser GroupSendEndorsementsResponse) ReceiveWithServiceIDs(
groupMembers []ServiceID, localUser ServiceID, params *GroupSecretParams, spp *ServerPublicParams,
) (GroupSendEndorsement, map[ServiceID]GroupSendEndorsement, error) {
var out C.SignalBytestringArray = C.SignalBytestringArray{}
concatenatedMembers := make([]byte, len(groupMembers)*ServiceIDFixedBytesLength)
for i, member := range groupMembers {
copy(concatenatedMembers[i*ServiceIDFixedBytesLength:(i+1)*ServiceIDFixedBytesLength], member.FixedBytes()[:])
}
signalFfiError := C.signal_group_send_endorsements_response_receive_and_combine_with_service_ids(
&out,
BytesToBuffer(gser),
BytesToBuffer(concatenatedMembers),
localUser.cConstFixedArray(),
C.uint64_t(time.Now().Unix()),
params.cConstFixedArray(),
C.SignalConstPointerServerPublicParams{spp},
)
runtime.KeepAlive(gser)
runtime.KeepAlive(concatenatedMembers)
runtime.KeepAlive(params)
runtime.KeepAlive(spp)
if signalFfiError != nil {
return nil, nil, wrapError(signalFfiError)
}
endorsements := CopySignalBytestringArray[GroupSendEndorsement](out)
memberEndorsements := make(map[ServiceID]GroupSendEndorsement, len(groupMembers))
for i, member := range groupMembers {
if len(endorsements) > i && len(endorsements[i]) > 0 {
memberEndorsements[member] = endorsements[i]
}
}
combined, err := GroupSendEndorsementCombine(endorsements...)
if err != nil {
return nil, memberEndorsements, err
}
return combined, memberEndorsements, nil
}

View file

@ -84,7 +84,8 @@ func (i *IdentityKey) VerifyAlternateIdentity(other *IdentityKey, signature []by
}
func (i *IdentityKey) Equal(other *IdentityKey) (bool, error) {
return i.publicKey.Equal(other.publicKey)
result, err := i.publicKey.Compare(other.publicKey)
return result == 0, err
}
type IdentityKeyPair struct {

View file

@ -20,12 +20,14 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *keyp);
typedef const SignalProtocolAddress const_address;
typedef const SignalPublicKey const_public_key;
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPrivateKey **keyp);
extern int signal_get_local_registration_id_callback(void *store_ctx, uint32_t *idp);
extern int signal_save_identity_key_callback(void *store_ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
extern int signal_get_identity_key_callback(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address);
extern int signal_is_trusted_identity_callback(void *store_ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
extern int signal_save_identity_key_callback(void *store_ctx, const_address *address, const_public_key *public_key);
extern int signal_get_identity_key_callback(void *store_ctx, SignalPublicKey **public_keyp, const_address *address);
extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
*/
import "C"
import (
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
}
//export signal_get_identity_key_pair_callback
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp *C.SignalPairOfMutPointerPrivateKeyMutPointerPublicKey) C.int {
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp **C.SignalPrivateKey) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
key, err := store.GetIdentityKeyPair(ctx)
if err != nil {
return err
}
if key == nil {
keyp.first.raw = nil
keyp.second.raw = nil
return nil
}
privClone, err := key.privateKey.Clone()
*keyp = nil
} else {
clone, err := key.privateKey.Clone()
if err != nil {
return err
}
pubClone, err := key.publicKey.Clone()
if err != nil {
return err
clone.CancelFinalizer()
*keyp = clone.ptr
}
privClone.CancelFinalizer()
pubClone.CancelFinalizer()
keyp.first.raw = privClone.ptr
keyp.second.raw = pubClone.ptr
return err
})
}
@ -88,17 +83,17 @@ func signal_get_local_registration_id_callback(storeCtx unsafe.Pointer, idp *C.u
}
//export signal_save_identity_key_callback
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, address C.SignalMutPointerProtocolAddress, publicKey C.SignalMutPointerPublicKey) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
publicKeyStruct := PublicKey{ptr: publicKey.raw}
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, address *C.const_address, publicKey *C.const_public_key) C.int {
return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
publicKeyStruct := PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(publicKey))}
cloned, err := publicKeyStruct.Clone()
if err != nil {
return err
return -1, err
}
addr := &Address{ptr: address.raw}
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID()
if err != nil {
return err
return -1, err
}
replaced, err := store.SaveIdentityKey(
ctx,
@ -106,21 +101,20 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t,
&IdentityKey{cloned},
)
if err != nil {
return err
return -1, err
}
if replaced {
*out = 1
return 1, nil
} else {
*out = 0
return 0, nil
}
return nil
})
}
//export signal_get_identity_key_callback
func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.SignalMutPointerPublicKey, address C.SignalMutPointerProtocolAddress) C.int {
func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp **C.SignalPublicKey, address *C.const_address) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
addr := &Address{ptr: address.raw}
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID()
if err != nil {
return err
@ -128,42 +122,39 @@ func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.Si
key, err := store.GetIdentityKey(ctx, theirServiceID)
if err == nil && key != nil {
key.publicKey.CancelFinalizer()
public_keyp.raw = key.publicKey.ptr
*public_keyp = key.publicKey.ptr
}
return err
})
}
//export signal_is_trusted_identity_callback
func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, out *C.bool, address C.SignalMutPointerProtocolAddress, public_key C.SignalMutPointerPublicKey, direction C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
addr := &Address{ptr: address.raw}
func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, address *C.const_address, public_key *C.const_public_key, direction C.uint) C.int {
return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID()
if err != nil {
return err
return -1, err
}
trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: public_key.raw}}, SignalDirection(direction))
trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(public_key))}}, SignalDirection(direction))
if err != nil {
return err
return -1, err
}
if trusted {
return 1, nil
} else {
return 0, nil
}
*out = C.bool(trusted)
return nil
})
}
//export signal_destroy_identity_key_store_callback
func signal_destroy_identity_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct {
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
ctx: wrapStore(ctx, store),
get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback),
get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback),
save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback),
is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback),
destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
get_identity_key_pair: C.SignalGetIdentityKeyPair(C.signal_get_identity_key_pair_callback),
get_local_registration_id: C.SignalGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
save_identity: C.SignalSaveIdentityKey(C.signal_save_identity_key_callback),
get_identity: C.SignalGetIdentityKey(C.signal_get_identity_key_callback),
is_trusted_identity: C.SignalIsTrustedIdentity(C.signal_is_trusted_identity_callback),
}}
}

View file

@ -20,10 +20,11 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id);
extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
extern void signal_destroy_kyber_pre_key_store_callback(void *store_ctx);
typedef const SignalKyberPreKeyRecord const_kyber_pre_key_record;
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalKyberPreKeyRecord **recordp, uint32_t id);
extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, const_kyber_pre_key_record *record);
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id);
*/
import "C"
import (
@ -38,21 +39,21 @@ type KyberPreKeyStore interface {
}
//export signal_load_kyber_pre_key_callback
func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerKyberPreKeyRecord, id C.uint32_t) C.int {
func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalKyberPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
key, err := store.LoadKyberPreKey(ctx, uint32(id))
if err == nil && key != nil {
key.CancelFinalizer()
keyp.raw = key.ptr
*keyp = key.ptr
}
return err
})
}
//export signal_store_kyber_pre_key_callback
func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerKyberPreKeyRecord) C.int {
func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_kyber_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
record := KyberPreKeyRecord{ptr: preKeyRecord.raw}
record := KyberPreKeyRecord{ptr: (*C.SignalKyberPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone()
if err != nil {
return err
@ -62,24 +63,18 @@ func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t,
}
//export signal_mark_kyber_pre_key_used_callback
func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t, ecPrekeyID C.uint32_t, baseKey C.SignalMutPointerPublicKey) C.int {
func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
// TODO use ecPrekeyID and baseKey?
return store.MarkKyberPreKeyUsed(ctx, uint32(id))
err := store.MarkKyberPreKeyUsed(ctx, uint32(id))
return err
})
}
//export signal_destroy_kyber_pre_key_store_callback
func signal_destroy_kyber_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
ctx: wrapStore(ctx, store),
load_kyber_pre_key: C.SignalFfiKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
store_kyber_pre_key: C.SignalFfiKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
mark_kyber_pre_key_used: C.SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
destroy: C.SignalFfiKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
load_kyber_pre_key: C.SignalLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
store_kyber_pre_key: C.SignalStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
mark_kyber_pre_key_used: C.SignalMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
}}
}

@ -1 +1 @@
Subproject commit 857c4dca03537dc5e395a5e1eda6bf18f59c3601
Subproject commit c02859f57552477680fb7928c3c3426193040313

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,8 @@ package libsignalgo
/*
#include <./libsignal-ffi.h>
extern void signal_log_callback(void *ctx, SignalLogLevel level, SignalCStringPtr file, uint32_t line, SignalCStringPtr message);
extern void signal_log_callback(void *ctx, SignalLogLevel level, char *file, uint32_t line, char *message);
extern void signal_log_flush_callback(void *ctx);
extern void signal_log_destroy_callback(void *ctx);
*/
import "C"
import (
@ -32,8 +31,8 @@ import (
var ffiLogger Logger
//export signal_log_callback
func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file C.SignalCStringPtr, line C.uint32_t, message C.SignalCStringPtr) {
ffiLogger.Log(LogLevel(int(level)), CopyCStringToString(file), uint(line), CopyCStringToString(message))
func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file *C.char, line C.uint32_t, message *C.char) {
ffiLogger.Log(LogLevel(int(level)), C.GoString(file), uint(line), C.GoString(message))
}
//export signal_log_flush_callback
@ -41,11 +40,6 @@ func signal_log_flush_callback(ctx unsafe.Pointer) {
ffiLogger.Flush()
}
//export signal_log_destroy_callback
func signal_log_destroy_callback(ctx unsafe.Pointer) {
ffiLogger.Destroy()
}
type LogLevel int
const (
@ -59,14 +53,12 @@ const (
type Logger interface {
Log(level LogLevel, file string, line uint, message string)
Flush()
Destroy()
}
func InitLogger(level LogLevel, logger Logger) {
ffiLogger = logger
C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLoggerStruct{
log: C.SignalFfiLoggerLog(C.signal_log_callback),
flush: C.SignalFfiLoggerFlush(C.signal_log_flush_callback),
destroy: C.SignalFfiLoggerDestroy(C.signal_log_destroy_callback),
C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLogger{
log: C.SignalLogCallback(C.signal_log_callback),
flush: C.SignalLogFlushCallback(C.signal_log_flush_callback),
})
}

View file

@ -27,7 +27,7 @@ import (
"time"
)
func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) {
func Encrypt(ctx context.Context, plaintext []byte, forAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) {
var ciphertextMessage C.SignalMutPointerCiphertextMessage
var now C.uint64_t = C.uint64_t(time.Now().Unix())
callbackCtx := NewCallbackContext(ctx)
@ -36,7 +36,6 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
&ciphertextMessage,
BytesToBuffer(plaintext),
forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityKeyStore),
now,
@ -49,7 +48,7 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
return wrapCiphertextMessage(ciphertextMessage.raw), nil
}
func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
func Decrypt(ctx context.Context, message *Message, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -57,7 +56,6 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
&decrypted,
message.constPtr(),
fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore),
)
@ -71,10 +69,10 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
type Message struct {
nc noCopy
ptr *C.SignalSignalMessage
ptr *C.SignalMessage
}
func wrapMessage(ptr *C.SignalSignalMessage) *Message {
func wrapMessage(ptr *C.SignalMessage) *Message {
message := &Message{ptr: ptr}
runtime.SetFinalizer(message, (*Message).Destroy)
return message
@ -156,3 +154,22 @@ func (m *Message) GetCounter() (uint32, error) {
}
return uint32(counter), nil
}
func (m *Message) VerifyMAC(sender, receiver *PublicKey, macKey []byte) (bool, error) {
var result C.bool
signalFfiError := C.signal_message_verify_mac(
&result,
m.constPtr(),
sender.constPtr(),
receiver.constPtr(),
BytesToBuffer(macKey),
)
runtime.KeepAlive(m)
runtime.KeepAlive(sender)
runtime.KeepAlive(receiver)
runtime.KeepAlive(macKey)
if signalFfiError != nil {
return false, wrapError(signalFfiError)
}
return bool(result), nil
}

View file

@ -20,17 +20,16 @@ package libsignalgo
#include "./libsignal-ffi.h"
*/
import "C"
import "runtime"
import (
"runtime"
"unsafe"
)
type MessageBackupKey struct {
nc noCopy
ptr *C.SignalMessageBackupKey
}
const MessageBackupKeyBytesLength = 32
type messageBackupKeyBytes = fixedArray32
func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
backupKey := &MessageBackupKey{ptr: ptr}
runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy)
@ -39,12 +38,10 @@ func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceID) (*MessageBackupKey, error) {
var bk C.SignalMutPointerMessageBackupKey
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_message_backup_key_from_account_entropy_pool(
&bk,
aepC,
aci.cConstFixedArray(),
C.CString(string(aep)),
aci.CFixedBytes(),
nil, // TODO what's a forward secrecy token?
)
runtime.KeepAlive(aep)
@ -58,8 +55,8 @@ func MessageBackupKeyFromBackupKeyAndID(backupKey *BackupKey, backupID *BackupID
var bk C.SignalMutPointerMessageBackupKey
signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id(
&bk,
backupKey.cConstFixedArray(),
backupID.cConstFixedArray(),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(backupKey)),
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(backupID)),
nil, // TODO what's a forward secrecy token?
)
runtime.KeepAlive(backupKey)
@ -83,26 +80,26 @@ func (bk *MessageBackupKey) Destroy() error {
return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr()))
}
func (bk *MessageBackupKey) GetHMACKey() ([MessageBackupKeyBytesLength]byte, error) {
var out messageBackupKeyBytes
func (bk *MessageBackupKey) GetHMACKey() ([32]byte, error) {
var out [32]byte
signalFfiError := C.signal_message_backup_key_get_hmac_key(
out.cFixedArray(),
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
bk.constPtr(),
)
if signalFfiError != nil {
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
return out, wrapError(signalFfiError)
}
return [MessageBackupKeyBytesLength]byte(out), nil
return out, nil
}
func (bk *MessageBackupKey) GetAESKey() ([MessageBackupKeyBytesLength]byte, error) {
var out messageBackupKeyBytes
func (bk *MessageBackupKey) GetAESKey() ([32]byte, error) {
var out [32]byte
signalFfiError := C.signal_message_backup_key_get_aes_key(
out.cFixedArray(),
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
bk.constPtr(),
)
if signalFfiError != nil {
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
return out, wrapError(signalFfiError)
}
return [MessageBackupKeyBytesLength]byte(out), nil
return out, nil
}

View file

@ -26,7 +26,7 @@ import (
"runtime"
)
func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore, preKeyStore PreKeyStore, signedPreKeyStore SignedPreKeyStore, kyberPreKeyStore KyberPreKeyStore) ([]byte, error) {
func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore, preKeyStore PreKeyStore, signedPreKeyStore SignedPreKeyStore, kyberPreKeyStore KyberPreKeyStore) ([]byte, error) {
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -34,7 +34,6 @@ func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddres
&decrypted,
preKeyMessage.constPtr(),
fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore),
callbackCtx.wrapPreKeyStore(preKeyStore),

View file

@ -27,14 +27,13 @@ import (
"time"
)
func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error {
func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error {
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
var now C.uint64_t = C.uint64_t(time.Now().Unix())
signalFfiError := C.signal_process_prekey_bundle(
bundle.constPtr(),
forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore),
now,

View file

@ -20,10 +20,11 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_load_pre_key_callback(void *store_ctx, SignalMutPointerPreKeyRecord *recordp, uint32_t id);
extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
typedef const SignalPreKeyRecord const_pre_key_record;
extern int signal_load_pre_key_callback(void *store_ctx, SignalPreKeyRecord **recordp, uint32_t id);
extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, const_pre_key_record *record);
extern int signal_remove_pre_key_callback(void *store_ctx, uint32_t id);
extern void signal_destroy_pre_key_store_callback(void *store_ctx);
*/
import "C"
import (
@ -38,21 +39,21 @@ type PreKeyStore interface {
}
//export signal_load_pre_key_callback
func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerPreKeyRecord, id C.uint32_t) C.int {
func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
key, err := store.LoadPreKey(ctx, uint32(id))
if err == nil && key != nil {
key.CancelFinalizer()
keyp.raw = key.ptr
*keyp = key.ptr
}
return err
})
}
//export signal_store_pre_key_callback
func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerPreKeyRecord) C.int {
func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
record := PreKeyRecord{ptr: preKeyRecord.raw}
record := PreKeyRecord{ptr: (*C.SignalPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone()
if err != nil {
return err
@ -68,17 +69,11 @@ func signal_remove_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.in
})
}
//export signal_destroy_pre_key_store_callback
func signal_destroy_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct {
return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{
ctx: wrapStore(ctx, store),
load_pre_key: C.SignalFfiPreKeyStoreLoadPreKey(C.signal_load_pre_key_callback),
store_pre_key: C.SignalFfiPreKeyStoreStorePreKey(C.signal_store_pre_key_callback),
remove_pre_key: C.SignalFfiPreKeyStoreRemovePreKey(C.signal_remove_pre_key_callback),
destroy: C.SignalFfiPreKeyStoreDestroy(C.signal_destroy_pre_key_store_callback),
load_pre_key: C.SignalLoadPreKey(C.signal_load_pre_key_callback),
store_pre_key: C.SignalStorePreKey(C.signal_store_pre_key_callback),
remove_pre_key: C.SignalRemovePreKey(C.signal_remove_pre_key_callback),
}}
}

View file

@ -23,7 +23,6 @@ package libsignalgo
*/
import "C"
import (
"encoding/base64"
"errors"
"runtime"
"unsafe"
@ -32,38 +31,12 @@ import (
"go.mau.fi/util/random"
)
const ProfileKeyLength = 32
const AccessKeyLength = 16
const ProfileKeyVersionLength = 64
const ProfileKeyLength = C.SignalPROFILE_KEY_LEN
type ProfileKey [ProfileKeyLength]byte
type ProfileKeyCommitment = fixedArray97
type ProfileKeyVersion [ProfileKeyVersionLength]byte
type AccessKey [AccessKeyLength]byte
func (pk *ProfileKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(pk))
}
func (pk *ProfileKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(pk.cFixedArray())
}
func (pkv *ProfileKeyVersion) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(pkv))
}
func (pkv *ProfileKeyVersion) cConstFixedArray() cFixedArray64Compat {
return cFixedArray64Compat(pkv.cFixedArray())
}
func (ak *AccessKey) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(ak))
}
func (ak *AccessKey) cConstFixedArray() cFixedArray16Compat {
return cFixedArray16Compat(ak.cFixedArray())
}
type ProfileKeyCommitment [C.SignalPROFILE_KEY_COMMITMENT_LEN]byte
type ProfileKeyVersion [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]byte
type AccessKey [C.SignalACCESS_KEY_LEN]byte
func DeserializeProfileKey(bytes []byte) (*ProfileKey, error) {
if len(bytes) == 0 {
@ -81,6 +54,10 @@ func (pk *ProfileKey) IsEmpty() bool {
return pk == nil || *pk == blankProfileKey
}
func (ak *AccessKey) String() string {
return string(ak[:])
}
func (pv *ProfileKeyVersion) String() string {
return string(pv[:])
}
@ -92,30 +69,14 @@ func (pk *ProfileKey) Slice() []byte {
return pk[:]
}
func (ak *AccessKey) Xor(other *AccessKey) *AccessKey {
if ak == nil {
return other
} else if other == nil {
return ak
}
var result AccessKey
for i := 0; i < AccessKeyLength; i++ {
result[i] = ak[i] ^ other[i]
}
return &result
}
func (ak *AccessKey) String() string {
return base64.StdEncoding.EncodeToString(ak[:])
}
func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) {
var result ProfileKeyCommitment
c_uuid := NewACIServiceID(u).cConstFixedArray()
c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
c_uuid := NewACIServiceID(u).CFixedBytes()
signalFfiError := C.signal_profile_key_get_commitment(
result.cFixedArray(),
pk.cConstFixedArray(),
&c_result,
c_profileKey,
c_uuid,
)
runtime.KeepAlive(pk)
@ -125,16 +86,19 @@ func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error)
return nil, wrapError(signalFfiError)
}
var result ProfileKeyCommitment
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_COMMITMENT_LEN)))
return &result, nil
}
func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, error) {
var result ProfileKeyVersion
c_uuid := NewACIServiceID(u).cConstFixedArray()
c_result := [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
c_uuid := NewACIServiceID(u).CFixedBytes()
signalFfiError := C.signal_profile_key_get_profile_key_version(
result.cFixedArray(),
pk.cConstFixedArray(),
&c_result,
c_profileKey,
c_uuid,
)
runtime.KeepAlive(pk)
@ -144,15 +108,18 @@ func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, err
return nil, wrapError(signalFfiError)
}
var result ProfileKeyVersion
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_VERSION_ENCODED_LEN)))
return &result, nil
}
func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
var result AccessKey
c_result := [C.SignalACCESS_KEY_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
signalFfiError := C.signal_profile_key_derive_access_key(
result.cFixedArray(),
pk.cConstFixedArray(),
&c_result,
c_profileKey,
)
runtime.KeepAlive(pk)
@ -160,35 +127,31 @@ func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
return nil, wrapError(signalFfiError)
}
var result AccessKey
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalACCESS_KEY_LEN)))
return &result, nil
}
type ProfileKeyCredentialRequestContext [473]byte
type ProfileKeyCredentialRequest = fixedArray329
type ProfileKeyCredentialRequestContext [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]byte
type ProfileKeyCredentialRequest [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]byte
type ProfileKeyCredentialResponse []byte
type ProfileKeyCredentialPresentation []byte
type ExpiringProfileKeyCredential = fixedArray153
type ExpiringProfileKeyCredentialResponse = fixedArray497
func (p *ProfileKeyCredentialRequestContext) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(p))
}
func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() cFixedArray473Compat {
return cFixedArray473Compat(p.cFixedArray())
}
type ExpiringProfileKeyCredential [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]byte
type ExpiringProfileKeyCredentialResponse [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]byte
func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) {
var result ProfileKeyCredentialRequestContext
randBytes := Randomness(random.Bytes(RandomnessLength))
c_uuid := NewACIServiceID(u).cConstFixedArray()
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar{}
randBytes := [32]byte(random.Bytes(32))
c_random := (*[32]C.uchar)(unsafe.Pointer(&randBytes[0]))
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(&profileKey[0]))
c_uuid := NewACIServiceID(u).CFixedBytes()
signalFfiError := C.signal_server_public_params_create_profile_key_credential_request_context_deterministic(
result.cFixedArray(),
&c_result,
C.SignalConstPointerServerPublicParams{serverPublicParams},
randBytes.cConstFixedArray(),
c_random,
c_uuid,
profileKey.cConstFixedArray(),
c_profileKey,
)
runtime.KeepAlive(u)
runtime.KeepAlive(profileKey)
@ -196,20 +159,23 @@ func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicPa
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
result := ProfileKeyCredentialRequestContext(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN)))
return &result, nil
}
func (p *ProfileKeyCredentialRequestContext) ProfileKeyCredentialRequestContextGetRequest() (*ProfileKeyCredentialRequest, error) {
var result ProfileKeyCredentialRequest
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]C.uchar{}
c_context := (*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(p))
signalFfiError := C.signal_profile_key_credential_request_context_get_request(
result.cFixedArray(),
p.cConstFixedArray(),
&c_result,
c_context,
)
runtime.KeepAlive(p)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
result := ProfileKeyCredentialRequest(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN)))
return &result, nil
}
@ -225,12 +191,12 @@ func NewExpiringProfileKeyCredentialResponse(b []byte) (*ExpiringProfileKeyCrede
}
func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext *ProfileKeyCredentialRequestContext, response *ExpiringProfileKeyCredentialResponse, currentTimeInSeconds uint64) (*ExpiringProfileKeyCredential, error) {
var credential ExpiringProfileKeyCredential
c_credential := [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar{}
signalFfiError := C.signal_server_public_params_receive_expiring_profile_key_credential(
credential.cFixedArray(),
&c_credential,
C.SignalConstPointerServerPublicParams{spp},
requestContext.cConstFixedArray(),
response.cConstFixedArray(),
(*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(requestContext)),
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]C.uchar)(unsafe.Pointer(response)),
(C.uint64_t)(currentTimeInSeconds),
)
runtime.KeepAlive(requestContext)
@ -239,6 +205,8 @@ func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
credential := ExpiringProfileKeyCredential{}
copy(credential[:], C.GoBytes(unsafe.Pointer(&c_credential), C.int(C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN)))
return &credential, nil
}
@ -249,21 +217,25 @@ func (a ProfileKeyCredentialPresentation) CheckValidContents() error {
}
func (a ProfileKeyCredentialPresentation) UUIDCiphertext() (UUIDCiphertext, error) {
var out UUIDCiphertext
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(out.cFixedArray(), BytesToBuffer(a))
out := [C.SignalUUID_CIPHERTEXT_LEN]C.uchar{}
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(&out, BytesToBuffer(a))
runtime.KeepAlive(a)
if signalFfiError != nil {
return UUIDCiphertext{}, wrapError(signalFfiError)
}
return out, nil
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return result, nil
}
func (a ProfileKeyCredentialPresentation) ProfileKeyCiphertext() (ProfileKeyCiphertext, error) {
var out ProfileKeyCiphertext
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(out.cFixedArray(), BytesToBuffer(a))
out := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(&out, BytesToBuffer(a))
runtime.KeepAlive(a)
if signalFfiError != nil {
return ProfileKeyCiphertext{}, wrapError(signalFfiError)
}
return out, nil
var result ProfileKeyCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
return result, nil
}

View file

@ -29,9 +29,6 @@ type PublicKey struct {
}
func wrapPublicKey(ptr *C.SignalPublicKey) *PublicKey {
if ptr == nil {
return nil
}
publicKey := &PublicKey{ptr: ptr}
runtime.SetFinalizer(publicKey, (*PublicKey).Destroy)
return publicKey
@ -84,15 +81,15 @@ func (k *PublicKey) CancelFinalizer() {
runtime.SetFinalizer(k, nil)
}
func (k *PublicKey) Equal(other *PublicKey) (bool, error) {
var comparison C.bool
signalFfiError := C.signal_publickey_equals(&comparison, k.constPtr(), other.constPtr())
func (k *PublicKey) Compare(other *PublicKey) (int, error) {
var comparison C.int
signalFfiError := C.signal_publickey_compare(&comparison, k.constPtr(), other.constPtr())
runtime.KeepAlive(k)
runtime.KeepAlive(other)
if signalFfiError != nil {
return false, wrapError(signalFfiError)
return 0, wrapError(signalFfiError)
}
return bool(comparison), nil
return int(comparison), nil
}
func (k *PublicKey) Bytes() ([]byte, error) {

View file

@ -23,9 +23,7 @@ package libsignalgo
import "C"
import (
"context"
"fmt"
"runtime"
"unsafe"
"github.com/google/uuid"
)
@ -44,17 +42,8 @@ func NewSealedSenderAddress(e164 string, uuid uuid.UUID, deviceID uint32) *Seale
}
}
func SealedSenderEncryptPlaintext(
ctx context.Context,
message []byte,
contentHint UnidentifiedSenderMessageContentHint,
forAddress, localAddress *Address,
fromSenderCert *SenderCertificate,
sessionStore SessionStore,
identityStore IdentityKeyStore,
groupID *GroupIdentifier,
) ([]byte, error) {
ciphertextMessage, err := Encrypt(ctx, message, forAddress, localAddress, sessionStore, identityStore)
func SealedSenderEncryptPlaintext(ctx context.Context, message []byte, contentHint UnidentifiedSenderMessageContentHint, forAddress *Address, fromSenderCert *SenderCertificate, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
ciphertextMessage, err := Encrypt(ctx, message, forAddress, sessionStore, identityStore)
if err != nil {
return nil, err
}
@ -63,7 +52,7 @@ func SealedSenderEncryptPlaintext(
ciphertextMessage,
fromSenderCert,
contentHint,
groupID,
nil,
)
if err != nil {
return nil, err
@ -89,51 +78,8 @@ func SealedSenderEncrypt(ctx context.Context, usmc *UnidentifiedSenderMessageCon
return CopySignalOwnedBufferToBytes(encrypted), nil
}
type SessionAddressTuple struct {
ServiceID ServiceID
DeviceID int
Address *Address
Record *SessionRecord
}
func SealedSenderMultiRecipientEncrypt(
ctx context.Context,
usmc *UnidentifiedSenderMessageContent,
recipients []SessionAddressTuple,
identityStore IdentityKeyStore,
) ([]byte, error) {
var encrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
recipientAddresses := make([]C.SignalConstPointerProtocolAddress, len(recipients))
recipientSessions := make([]C.SignalConstPointerSessionRecord, len(recipients))
for i, recipient := range recipients {
recipientAddresses[i] = recipient.Address.constPtr()
recipientSessions[i] = recipient.Record.constPtr()
}
signalFfiError := C.signal_sealed_sender_multi_recipient_encrypt(
&encrypted,
C.SignalBorrowedSliceOfConstPointerProtocolAddress{
base: unsafe.SliceData(recipientAddresses),
length: C.size_t(len(recipientAddresses)),
},
C.SignalBorrowedSliceOfConstPointerSessionRecord{
base: unsafe.SliceData(recipientSessions),
length: C.size_t(len(recipientSessions)),
},
BytesToBuffer(nil),
usmc.constPtr(),
callbackCtx.wrapIdentityKeyStore(identityStore),
)
runtime.KeepAlive(usmc)
runtime.KeepAlive(recipients)
runtime.KeepAlive(recipientAddresses)
runtime.KeepAlive(recipientSessions)
if signalFfiError != nil {
return nil, callbackCtx.wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(encrypted), nil
func SealedSenderMultiRecipientEncrypt(messageContent *UnidentifiedSenderMessageContent, forRecipients []*Address, identityStore IdentityKeyStore, sessionStore SessionStore, ctx *CallbackContext) ([]byte, error) {
panic("not implemented")
}
type SealedSenderResult struct {
@ -180,22 +126,18 @@ func wrapUnidentifiedSenderMessageContent(ptr *C.SignalUnidentifiedSenderMessage
return messageContent
}
func NewUnidentifiedSenderMessageContent(message *CiphertextMessage, senderCertificate *SenderCertificate, contentHint UnidentifiedSenderMessageContentHint, groupID *GroupIdentifier) (*UnidentifiedSenderMessageContent, error) {
func NewUnidentifiedSenderMessageContent(message *CiphertextMessage, senderCertificate *SenderCertificate, contentHint UnidentifiedSenderMessageContentHint, groupID []byte) (*UnidentifiedSenderMessageContent, error) {
var usmc C.SignalMutPointerUnidentifiedSenderMessageContent
var groupIDBytes []byte
if groupID != nil {
groupIDBytes = groupID[:]
}
signalFfiError := C.signal_unidentified_sender_message_content_new(
&usmc,
message.constPtr(),
senderCertificate.constPtr(),
C.uint32_t(contentHint),
BytesToBuffer(groupIDBytes),
BytesToBuffer(groupID),
)
runtime.KeepAlive(message)
runtime.KeepAlive(senderCertificate)
runtime.KeepAlive(groupIDBytes)
runtime.KeepAlive(groupID)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
@ -267,21 +209,18 @@ func (usmc *UnidentifiedSenderMessageContent) GetContents() ([]byte, error) {
return CopySignalOwnedBufferToBytes(contents), nil
}
func (usmc *UnidentifiedSenderMessageContent) GetGroupID() (*GroupIdentifier, error) {
var contents C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_unidentified_sender_message_content_get_group_id_or_empty(&contents, usmc.constPtr())
runtime.KeepAlive(usmc)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
bytes := CopySignalOwnedBufferToBytes(contents)
if len(bytes) == 0 {
return nil, nil
} else if len(bytes) != GroupIdentifierLength {
return nil, fmt.Errorf("unexpected group ID length: %d", len(bytes))
}
return (*GroupIdentifier)(bytes), nil
}
//func (usmc *UnidentifiedSenderMessageContent) GetGroupID() ([]byte, error) {
// var groupID *C.uchar
// var length C.ulong
// signalFfiError := C.signal_unidentified_sender_message_content_get_group_id(&groupID, &length, usmc.ptr)
// if signalFfiError != nil {
// return nil, wrapError(signalFfiError)
// }
// if groupID == nil {
// return nil, nil
// }
// return CopyBufferToBytes(groupID, length), nil
//}
func (usmc *UnidentifiedSenderMessageContent) GetSenderCertificate() (*SenderCertificate, error) {
var senderCertificate C.SignalMutPointerSenderCertificate

View file

@ -44,14 +44,10 @@ func wrapSenderCertificate(ptr *C.SignalSenderCertificate) *SenderCertificate {
// the Swift bindings).
func NewSenderCertificate(sender *SealedSenderAddress, publicKey *PublicKey, expiration time.Time, signerCertificate *ServerCertificate, signerKey *PrivateKey) (*SenderCertificate, error) {
var sc C.SignalMutPointerSenderCertificate
senderUUIDStr, freeSenderUUIDStr := GoStringToCString(sender.UUID.String())
defer freeSenderUUIDStr()
senderE164Str, freeSenderE164Str := GoStringToCString(sender.E164)
defer freeSenderE164Str()
signalFfiError := C.signal_sender_certificate_new(
&sc,
senderUUIDStr,
senderE164Str,
C.CString(sender.UUID.String()),
C.CString(sender.E164),
C.uint32_t(sender.DeviceID),
publicKey.constPtr(),
C.uint64_t(expiration.UnixMilli()),
@ -139,7 +135,7 @@ func (sc *SenderCertificate) GetSignature() ([]byte, error) {
}
func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, error) {
var rawUUID C.SignalCStringPtr
var rawUUID *C.char
signalFfiError := C.signal_sender_certificate_get_sender_uuid(&rawUUID, sc.constPtr())
runtime.KeepAlive(sc)
if signalFfiError != nil {
@ -149,7 +145,7 @@ func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, error) {
}
func (sc *SenderCertificate) GetSenderE164() (string, error) {
var e164 C.SignalCStringPtr
var e164 *C.char
signalFfiError := C.signal_sender_certificate_get_sender_e164(&e164, sc.constPtr())
runtime.KeepAlive(sc)
if signalFfiError != nil {

View file

@ -60,7 +60,7 @@ func NewSenderKeyDistributionMessage(ctx context.Context, sender *Address, distr
signalFfiError := C.signal_sender_key_distribution_message_create(
&skdm,
sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)),
(*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
callbackCtx.wrapSenderKeyStore(store),
)
runtime.KeepAlive(sender)

View file

@ -20,9 +20,13 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_load_sender_key_callback(void *store_ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id);
extern int signal_store_sender_key_callback(void *store_ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
extern void signal_destroy_sender_key_store_callback(void *store_ctx);
typedef const SignalProtocolAddress const_address;
typedef const SignalSenderKeyRecord const_sender_key_record;
typedef const uint8_t const_uuid_bytes[16];
extern int signal_load_sender_key_callback(void *store_ctx, SignalSenderKeyRecord**, const_address*, const_uuid_bytes*);
extern int signal_store_sender_key_callback(void *store_ctx, const_address*, const_uuid_bytes*, const_sender_key_record*);
*/
import "C"
import (
@ -38,40 +42,36 @@ type SenderKeyStore interface {
}
//export signal_load_sender_key_callback
func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSenderKeyRecord, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid) C.int {
func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp **C.SignalSenderKeyRecord, address *C.const_address, distributionIDBytes *C.const_uuid_bytes) C.int {
return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
record, err := store.LoadSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID)))
distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
record, err := store.LoadSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID)
if err == nil && record != nil {
record.CancelFinalizer()
recordp.raw = record.ptr
*recordp = record.ptr
}
return err
})
}
//export signal_store_sender_key_callback
func signal_store_sender_key_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid, senderKeyRecord C.SignalMutPointerSenderKeyRecord) C.int {
func signal_store_sender_key_callback(storeCtx unsafe.Pointer, address *C.const_address, distributionIDBytes *C.const_uuid_bytes, senderKeyRecord *C.const_sender_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
record := SenderKeyRecord{ptr: senderKeyRecord.raw}
distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
record := SenderKeyRecord{ptr: (*C.SignalSenderKeyRecord)(unsafe.Pointer(senderKeyRecord))}
cloned, err := record.Clone()
if err != nil {
return err
}
return store.StoreSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), cloned)
return store.StoreSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID, cloned)
})
}
//export signal_destroy_sender_key_store_callback
func signal_destroy_sender_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct {
return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{
ctx: wrapStore(ctx, store),
load_sender_key: C.SignalFfiSenderKeyStoreLoadSenderKey(C.signal_load_sender_key_callback),
store_sender_key: C.SignalFfiSenderKeyStoreStoreSenderKey(C.signal_store_sender_key_callback),
destroy: C.SignalFfiSenderKeyStoreDestroy(C.signal_destroy_sender_key_store_callback),
load_sender_key: C.SignalLoadSenderKey(C.signal_load_sender_key_callback),
store_sender_key: C.SignalStoreSenderKey(C.signal_store_sender_key_callback),
}}
}

View file

@ -24,16 +24,15 @@ import "C"
import (
"fmt"
"runtime"
"unsafe"
)
type ServerPublicParams = C.SignalServerPublicParams
type NotarySignature = fixedArray64
const ServerPublicParamsLength = 673
type NotarySignature [C.SignalSIGNATURE_LEN]byte
func DeserializeServerPublicParams(params []byte) (*ServerPublicParams, error) {
if len(params) != ServerPublicParamsLength {
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), ServerPublicParamsLength)
if len(params) != C.SignalSERVER_PUBLIC_PARAMS_LEN {
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), int(C.SignalSERVER_PUBLIC_PARAMS_LEN))
}
var out C.SignalMutPointerServerPublicParams
signalFfiError := C.signal_server_public_params_deserialize(&out, BytesToBuffer(params[:]))
@ -48,10 +47,11 @@ func ServerPublicParamsVerifySignature(
messageBytes []byte,
NotarySignature NotarySignature,
) error {
c_notarySignature := (*[C.SignalSIGNATURE_LEN]C.uint8_t)(unsafe.Pointer(&NotarySignature[0]))
signalFfiError := C.signal_server_public_params_verify_signature(
C.SignalConstPointerServerPublicParams{serverPublicParams},
BytesToBuffer(messageBytes),
NotarySignature.cConstFixedArray(),
c_notarySignature,
)
runtime.KeepAlive(messageBytes)
return wrapError(signalFfiError)

View file

@ -31,6 +31,12 @@ import (
"github.com/rs/zerolog"
)
func init() {
if C.SignalUUID_LEN != 16 {
panic("libsignal-ffi uuid type size mismatch")
}
}
type ServiceIDType byte
const (
@ -87,9 +93,6 @@ func (s ServiceID) IsEmpty() bool {
}
func (s ServiceID) Address(deviceID uint) (*Address, error) {
if s.IsEmpty() {
return nil, fmt.Errorf("cannot create address from empty ServiceID")
}
return newAddress(s.String(), deviceID)
}
@ -115,28 +118,12 @@ func (s ServiceID) GoString() string {
return fmt.Sprintf(`libsignalgo.ServiceID{Type: %#v, UUID: uuid.MustParse("%s")}`, s.Type, s.UUID)
}
func (s ServiceID) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s *ServiceID) UnmarshalText(text []byte) error {
parsed, err := ServiceIDFromString(string(text))
if err != nil {
return err
}
*s = parsed
return nil
}
func (s ServiceID) MarshalZerologObject(e *zerolog.Event) {
e.Stringer("type", s.Type)
e.Stringer("uuid", s.UUID)
}
const ServiceIDUUIDLength = 16
const ServiceIDFixedBytesLength = 17
type ServiceIDFixedBytes = fixedArray17
type ServiceIDFixedBytes [17]byte
func (s ServiceID) FixedBytes() *ServiceIDFixedBytes {
var result ServiceIDFixedBytes
@ -164,19 +151,7 @@ func ServiceIDFromString(val string) (ServiceID, error) {
}
}
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) {
if len(bytes) == ServiceIDUUIDLength {
return NewACIServiceID(uuid.UUID(bytes)), nil
} else if len(bytes) == ServiceIDFixedBytesLength {
return ServiceID{
Type: ServiceIDType(bytes[0]),
UUID: uuid.UUID(bytes[1:]),
}, nil
}
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected %d or %d)", len(bytes), ServiceIDUUIDLength, ServiceIDFixedBytesLength)
}
func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) ServiceID {
func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
var id ServiceID
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))
id.Type = ServiceIDType(fixedBytes[0])
@ -184,10 +159,6 @@ func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) Serv
return id
}
func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
return s.FixedBytes().cFixedArray()
}
func (s ServiceID) cConstFixedArray() cFixedArray17Compat {
return cFixedArray17Compat(s.cFixedArray())
func (s ServiceID) CFixedBytes() cPNIType {
return cPNIType(unsafe.Pointer(s.FixedBytes()))
}

View file

@ -0,0 +1,11 @@
//go:build darwin || android || ios || (windows && arm64)
package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
type cPNIType = *C.SignalServiceIdFixedWidthBinaryBytes

View file

@ -0,0 +1,14 @@
//go:build !(darwin || android || ios || (windows && arm64))
package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
// Hack for https://github.com/golang/go/issues/7270
// The clang version is more correct, but doesn't work with gcc
type cPNIType = *[17]C.uint8_t

View file

@ -30,7 +30,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
)
func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress, aliceAddress *libsignalgo.Address) {
func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress *libsignalgo.Address) {
ctx := context.TODO()
bobPreKey, err := libsignalgo.GeneratePrivateKey()
@ -86,7 +86,7 @@ func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtoc
assert.NoError(t, err)
// Alice processes the bundle
err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceAddress, aliceStore, aliceStore)
err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err)
record, err := aliceStore.LoadSession(ctx, bobAddress)
@ -132,11 +132,11 @@ func TestSessionCipher(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
initializeSessions(t, aliceStore, bobStore, bobAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceAddress, aliceStore, aliceStore)
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err)
aliceCiphertextMessageType, err := aliceCiphertext.MessageType()
assert.NoError(t, err)
@ -147,13 +147,13 @@ func TestSessionCipher(t *testing.T) {
bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized)
assert.NoError(t, err)
bobPlaintext, err := libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
bobPlaintext, err := libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
assert.NoError(t, err)
assert.Equal(t, alicePlaintext, bobPlaintext)
bobPlaintext2 := []byte{23}
bobCiphertext2, err := libsignalgo.Encrypt(ctx, bobPlaintext2, aliceAddress, bobAddress, bobStore, bobStore)
bobCiphertext2, err := libsignalgo.Encrypt(ctx, bobPlaintext2, aliceAddress, bobStore, bobStore)
assert.NoError(t, err)
bobCiphertext2MessageType, err := bobCiphertext2.MessageType()
assert.NoError(t, err)
@ -163,7 +163,7 @@ func TestSessionCipher(t *testing.T) {
assert.NoError(t, err)
aliceCiphertext2, err := libsignalgo.DeserializeMessage(bobCiphertext2Serialized)
assert.NoError(t, err)
alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceAddress, aliceStore, aliceStore)
alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err)
assert.Equal(t, bobPlaintext2, alicePlaintext2)
}
@ -183,11 +183,11 @@ func TestSessionCipherWithBadStore(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := &BadInMemorySignalProtocolStore{NewInMemorySignalProtocolStore()}
initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress, aliceAddress)
initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceAddress, aliceStore, aliceStore)
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err)
aliceCiphertextMessageType, err := aliceCiphertext.MessageType()
assert.NoError(t, err)
@ -198,7 +198,7 @@ func TestSessionCipherWithBadStore(t *testing.T) {
bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized)
assert.NoError(t, err)
t.Skip("This test is broken") // TODO fix
_, err = libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
_, err = libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
require.Error(t, err)
assert.Equal(t, "Test error", err.Error())
}
@ -216,7 +216,7 @@ func TestSealedSenderEncrypt_Repeated(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
initializeSessions(t, aliceStore, bobStore, bobAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err)
@ -241,7 +241,7 @@ func TestSealedSenderEncrypt_Repeated(t *testing.T) {
}()
for i := 0; i < 100; i++ {
message := []byte(fmt.Sprintf("%04d vision", i))
ciphertext, err := libsignalgo.SealedSenderEncryptPlaintext(ctx, message, libsignalgo.UnidentifiedSenderMessageContentHintDefault, bobAddress, aliceAddress, senderCert, aliceStore, aliceStore, nil)
ciphertext, err := libsignalgo.SealedSenderEncryptPlaintext(ctx, message, libsignalgo.UnidentifiedSenderMessageContentHintDefault, bobAddress, senderCert, aliceStore, aliceStore)
require.NoError(t, err)
assert.NotNil(t, ciphertext)
}
@ -252,18 +252,15 @@ func TestArchiveSession(t *testing.T) {
ctx := context.TODO()
setupLogging()
aliceACI := uuid.New()
bobACI := uuid.New()
aliceAddress, err := libsignalgo.NewACIServiceID(aliceACI).Address(1)
assert.NoError(t, err)
bobAddress, err := libsignalgo.NewACIServiceID(bobACI).Address(1)
assert.NoError(t, err)
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
initializeSessions(t, aliceStore, bobStore, bobAddress)
session, err := aliceStore.LoadSession(ctx, bobAddress)
assert.NoError(t, err)
@ -318,7 +315,7 @@ func TestSealedSenderGroupCipher(t *testing.T) {
bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
initializeSessions(t, aliceStore, bobStore, bobAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err)

View file

@ -83,9 +83,6 @@ func (sr *SessionRecord) ArchiveCurrentState() error {
}
func (sr *SessionRecord) CurrentRatchetKeyMatches(key *PublicKey) (bool, error) {
if sr == nil || key == nil {
return false, nil
}
var result C.bool
signalFfiError := C.signal_session_record_current_ratchet_key_matches(
&result,

View file

@ -20,9 +20,11 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_load_session_callback(void *store_ctx, SignalMutPointerSessionRecord *recordp, SignalMutPointerProtocolAddress address);
extern int signal_store_session_callback(void *store_ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
extern void signal_destroy_session_store_callback(void *store_ctx);
typedef const SignalSessionRecord const_session_record;
typedef const SignalProtocolAddress const_address;
extern int signal_load_session_callback(void *store_ctx, SignalSessionRecord **recordp, const_address *address);
extern int signal_store_session_callback(void *store_ctx, const_address *address, const_session_record *record);
*/
import "C"
import (
@ -36,39 +38,33 @@ type SessionStore interface {
}
//export signal_load_session_callback
func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSessionRecord, address C.SignalMutPointerProtocolAddress) C.int {
func signal_load_session_callback(storeCtx unsafe.Pointer, recordp **C.SignalSessionRecord, address *C.const_address) C.int {
return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
record, err := store.LoadSession(ctx, &Address{ptr: address.raw})
record, err := store.LoadSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))})
if err == nil && record != nil {
record.CancelFinalizer()
recordp.raw = record.ptr
*recordp = record.ptr
}
return err
})
}
//export signal_store_session_callback
func signal_store_session_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, sessionRecord C.SignalMutPointerSessionRecord) C.int {
func signal_store_session_callback(storeCtx unsafe.Pointer, address *C.const_address, sessionRecord *C.const_session_record) C.int {
return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
record := SessionRecord{ptr: sessionRecord.raw}
record := SessionRecord{ptr: (*C.SignalSessionRecord)(unsafe.Pointer(sessionRecord))}
cloned, err := record.Clone()
if err != nil {
return err
}
return store.StoreSession(ctx, &Address{ptr: address.raw}, cloned)
return store.StoreSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, cloned)
})
}
//export signal_destroy_session_store_callback
func signal_destroy_session_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct {
return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{
ctx: wrapStore(ctx, store),
load_session: C.SignalFfiSessionStoreLoadSession(C.signal_load_session_callback),
store_session: C.SignalFfiSessionStoreStoreSession(C.signal_store_session_callback),
destroy: C.SignalFfiSessionStoreDestroy(C.signal_destroy_session_store_callback),
load_session: C.SignalLoadSession(C.signal_load_session_callback),
store_session: C.SignalStoreSession(C.signal_store_session_callback),
}}
}

View file

@ -54,8 +54,6 @@ func (FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, message
func (FFILogger) Flush() {}
func (FFILogger) Destroy() {}
var loggingSetup = false
func setupLogging() {

View file

@ -20,9 +20,10 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalMutPointerSignedPreKeyRecord *recordp, uint32_t id);
extern int signal_store_signed_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
extern void signal_destroy_signed_pre_key_store_callback(void *store_ctx);
typedef const SignalSignedPreKeyRecord const_signed_pre_key_record;
extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalSignedPreKeyRecord **recordp, uint32_t id);
extern int signal_store_signed_pre_key_callback(void *store_ctx, uint32_t id, const_signed_pre_key_record *record);
*/
import "C"
import (
@ -36,21 +37,21 @@ type SignedPreKeyStore interface {
}
//export signal_load_signed_pre_key_callback
func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerSignedPreKeyRecord, id C.uint32_t) C.int {
func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalSignedPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
key, err := store.LoadSignedPreKey(ctx, uint32(id))
if err == nil && key != nil {
key.CancelFinalizer()
keyp.raw = key.ptr
*keyp = key.ptr
}
return err
})
}
//export signal_store_signed_pre_key_callback
func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerSignedPreKeyRecord) C.int {
func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_signed_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
record := SignedPreKeyRecord{ptr: preKeyRecord.raw}
record := SignedPreKeyRecord{ptr: (*C.SignalSignedPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone()
if err != nil {
return err
@ -59,16 +60,10 @@ func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t
})
}
//export signal_destroy_signed_pre_key_store_callback
func signal_destroy_signed_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct {
return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{
ctx: wrapStore(ctx, store),
load_signed_pre_key: C.SignalFfiSignedPreKeyStoreLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
store_signed_pre_key: C.SignalFfiSignedPreKeyStoreStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
destroy: C.SignalFfiSignedPreKeyStoreDestroy(C.signal_destroy_signed_pre_key_store_callback),
load_signed_pre_key: C.SignalLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
store_signed_pre_key: C.SignalStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
}}
}

View file

@ -1,10 +1,11 @@
#!/bin/sh
cd /data
export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER=""
apk add --no-cache git make cmake protobuf-dev musl-dev g++ clang-dev
apk add --no-cache git make cmake protoc musl-dev g++ clang-dev cbindgen
cd libsignal
cargo build -p libsignal-ffi --release
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
cd ..
mv libsignal/target/release/libsignal_ffi.a .
cp libsignal/swift/Sources/SignalFfi/signal_ffi.h libsignal-ffi.h
mv libsignal/libsignal-ffi.h .
chown 1000:1000 libsignal_ffi.a libsignal-ffi.h version.go

View file

@ -28,11 +28,14 @@ echo "const Version = \"$(git describe --tags --always)\"" >> ../version.go
# Build libsignal
cargo build -p libsignal-ffi --release
# Regenerate the header file
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
# Navigate back to the original directory
cd "$ORIGINAL_DIR"
# Copy files from the libsignal directory
cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" .
cp "${LIBSIGNAL_DIRECTORY}/swift/Sources/SignalFfi/signal_ffi.h" libsignal-ffi.h
cp "${LIBSIGNAL_DIRECTORY}/libsignal-ffi.h" .
echo "Files copied successfully."

View file

@ -2,4 +2,4 @@
package libsignalgo
const Version = "v0.100.0"
const Version = "v0.84.0"

View file

@ -44,6 +44,7 @@ func (mc *MessageConverter) ToSignal(
portal *bridgev2.Portal,
evt *event.Event,
content *event.MessageEventContent,
timestamp uint64,
relaybotFormatted bool,
replyTo *database.Message,
) (*signalpb.DataMessage, error) {
@ -54,6 +55,7 @@ func (mc *MessageConverter) ToSignal(
}
dm := &signalpb.DataMessage{
Timestamp: &timestamp,
Preview: mc.convertURLPreviewToSignal(ctx, content),
}
if replyTo != nil {
@ -61,7 +63,7 @@ func (mc *MessageConverter) ToSignal(
if err == nil {
dm.Quote = &signalpb.DataMessage_Quote{
Id: proto.Uint64(messageID),
AuthorAciBinary: authorACI[:],
AuthorAci: proto.String(authorACI.String()),
Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
}
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
@ -110,9 +112,6 @@ func (mc *MessageConverter) ToSignal(
return nil, fmt.Errorf("failed to convert sticker: %w", err)
}
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker)
if dm.Sticker == nil {
var emoji *string
// TODO check for single grapheme cluster?
if len([]rune(content.Body)) == 1 {
@ -124,10 +123,10 @@ func (mc *MessageConverter) ToSignal(
PackId: make([]byte, 16),
PackKey: make([]byte, 32),
StickerId: proto.Uint32(0),
Data: att,
Emoji: emoji,
}
}
dm.Sticker.Data = att
case event.MsgLocation:
lat, lon, err := parseGeoURI(content.GeoURI)
if err != nil {

View file

@ -81,16 +81,6 @@ func BackupToDataMessage(ci *backuppb.ChatItem, attMap AttachmentMap) (*signalpb
Emoji: ti.StickerMessage.Sticker.Emoji,
Data: backupToSignalAttachment(ti.StickerMessage.Sticker.Data, 0, uuid.New(), attMap),
}
case *backuppb.ChatItem_Poll:
dm.PollCreate = &signalpb.DataMessage_PollCreate{
Question: &ti.Poll.Question,
AllowMultiple: &ti.Poll.AllowMultiple,
Options: exslices.CastFunc(ti.Poll.Options, func(from *backuppb.Poll_PollOption) string {
return from.Option
}),
}
// TODO handle votes
// TODO handle hasEnded somehow?
case *backuppb.ChatItem_RemoteDeletedMessage:
// TODO handle some other way? (also disappeared view-once messages)
return nil, nil
@ -248,7 +238,11 @@ func backupToSignalBodyRange(from *backuppb.BodyRange) *signalpb.BodyRange {
out.Length = &from.Length
switch av := from.AssociatedValue.(type) {
case *backuppb.BodyRange_MentionAci:
out.AssociatedValue = &signalpb.BodyRange_MentionAciBinary{MentionAciBinary: av.MentionAci}
// TODO confirm this is correct
if len(av.MentionAci) != 16 {
return nil
}
out.AssociatedValue = &signalpb.BodyRange_MentionAci{MentionAci: uuid.UUID(av.MentionAci).String()}
case *backuppb.BodyRange_Style_:
out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)}
}

View file

@ -22,11 +22,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -55,7 +51,7 @@ func calculateLength(dm *signalpb.DataMessage) int {
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
return 1
}
if dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil {
if dm.Sticker != nil {
return 1
}
length := len(dm.Attachments) + len(dm.Contact)
@ -79,13 +75,11 @@ func CanConvertSignal(dm *signalpb.DataMessage) bool {
}
const ViewOnceDisappearTimer = 5 * time.Minute
const matrixTextMaxLength = 30000 // approximate value to avoid hitting 64 KiB PDU size limit with HTML duplication
func (mc *MessageConverter) ToMatrix(
ctx context.Context,
client *signalmeow.Client,
portal *bridgev2.Portal,
sender uuid.UUID,
intent bridgev2.MatrixAPI,
dm *signalpb.DataMessage,
attMap AttachmentMap,
@ -114,20 +108,8 @@ func (mc *MessageConverter) ToMatrix(
// Don't allow any other parts in a sticker message
return cm
}
if dm.PollVote != nil {
cm.Parts = append(cm.Parts, mc.convertPollVoteToMatrix(ctx, sender, dm.PollVote))
return cm
}
if dm.PollCreate != nil {
cm.Parts = append(cm.Parts, mc.convertPollCreateToMatrix(dm.PollCreate))
return cm
}
if dm.PollTerminate != nil {
cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate))
return cm
}
for i, att := range dm.GetAttachments() {
if att.GetContentType() != "text/x-signal-plain" || att.GetSize() > matrixTextMaxLength {
if att.GetContentType() != "text/x-signal-plain" {
cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att, attMap))
} else {
longBody, err := mc.downloadSignalLongText(ctx, att, attMap)
@ -178,12 +160,9 @@ func (mc *MessageConverter) ToMatrix(
}
}
if dm.Quote != nil {
authorACI, err := signalmeow.ParseStringOrBinaryUUID(dm.Quote.GetAuthorAci(), dm.Quote.GetAuthorAciBinary())
authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
if err != nil {
zerolog.Ctx(ctx).Err(err).
Str("author_aci", dm.Quote.GetAuthorAci()).
Hex("author_aci_binary", dm.Quote.GetAuthorAciBinary()).
Msg("Failed to parse quote author ACI")
zerolog.Ctx(ctx).Err(err).Str("author_aci", dm.Quote.GetAuthorAci()).Msg("Failed to parse quote author ACI")
} else {
cm.ReplyTo = &networkid.MessageOptionalPartID{
MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()),
@ -344,7 +323,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
card.Add(vcard.FieldTelephone, &field)
}
if contact.GetAvatar().GetAvatar() != nil {
avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap, nil)
avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar")
} else {
@ -462,28 +441,31 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
},
}
}
// Signal stickers are 512x512, so tell Matrix clients to render them as 200x200 to match Signal
// https://github.com/signalapp/Signal-Desktop/blob/v7.77.0-beta.1/ts/components/conversation/Message.dom.tsx#L135
// Signal stickers are 512x512, so tell Matrix clients to render them as 256x256
if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 {
converted.Content.Info.Width = 200
converted.Content.Info.Height = 200
converted.Content.Info.Width = 256
converted.Content.Info.Height = 256
}
converted.Content.Body = sticker.GetEmoji()
if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) {
converted.Content.Info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10),
Emoji: sticker.GetEmoji(),
PackURL: fmt.Sprintf(PackURLFormat, sticker.GetPackId(), sticker.GetPackKey()),
}
}
converted.Type = event.EventSticker
converted.Content.MsgType = ""
if converted.Extra == nil {
converted.Extra = map[string]any{}
}
// TODO fetch full pack metadata like the old bridge did?
converted.Extra["fi.mau.signal.sticker"] = map[string]any{
"id": sticker.GetStickerId(),
"emoji": sticker.GetEmoji(),
"pack": map[string]any{
"id": sticker.GetPackId(),
"key": sticker.GetPackKey(),
},
}
return converted
}
func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) {
data, err := mc.downloadAttachment(ctx, att, attMap, nil)
data, err := mc.downloadAttachment(ctx, att, attMap)
if err != nil {
return nil, err
}
@ -509,9 +491,7 @@ func checkIfAttachmentExists(att *signalpb.AttachmentPointer, attMap AttachmentM
return nil
}
func (mc *MessageConverter) downloadAttachment(
ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap, into *os.File,
) ([]byte, error) {
func (mc *MessageConverter) downloadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) ([]byte, error) {
if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err
}
@ -522,19 +502,19 @@ func (mc *MessageConverter) downloadAttachment(
plaintextHash = target.GetPlaintextHash()
}
}
return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash, into)
return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash)
}
func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*bridgev2.ConvertedMessagePart, error) {
fileName := att.GetFileName()
content := &event.MessageEventContent{
Body: att.GetFileName(),
Info: &event.FileInfo{
MimeType: att.GetContentType(),
Width: int(att.GetWidth()),
Height: int(att.GetHeight()),
Size: int(att.GetSize()),
},
}
mimeType := att.GetContentType()
if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err
} else if mc.DirectMedia {
@ -561,7 +541,25 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
}
content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
} else {
err = mc.actuallyReuploadAttachment(ctx, content, att, attMap)
data, err := mc.downloadAttachment(ctx, att, attMap)
if err != nil {
return nil, err
}
if mimeType == "" {
mimeType = http.DetectContentType(data)
}
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mimeType)
if err != nil {
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
}
fileName += ".ogg"
mimeType = "audio/ogg"
content.MSC3245Voice = &event.MSC3245Voice{}
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
//content.MSC1767Audio = &event.MSC1767Audio{}
}
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, fileName, mimeType)
if err != nil {
return nil, err
}
@ -570,21 +568,16 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
content.Info.Blurhash = att.GetBlurHash()
content.Info.AnoaBlurhash = att.GetBlurHash()
}
plainMime, _, _ := mime.ParseMediaType(content.Info.MimeType)
// Supported mime types from https://github.com/signalapp/Signal-Desktop/blob/main/ts/util/GoogleChrome.std.ts
switch plainMime {
case "image/avif", "image/bmp", "image/gif", "image/jpeg", "image/webp", "image/x-xbitmap",
"image/vnd.microsoft.icon", "image/ico", "image/icon", "image/x-icon", "image/png", "image/apng":
switch strings.Split(mimeType, "/")[0] {
case "image":
content.MsgType = event.MsgImage
case "video/mp4", "video/ogg", "video/webm":
case "video":
content.MsgType = event.MsgVideo
default:
if strings.HasPrefix(plainMime, "audio/") && !strings.HasSuffix(plainMime, "aiff") {
case "audio":
content.MsgType = event.MsgAudio
} else {
default:
content.MsgType = event.MsgFile
}
}
var extra map[string]any
if att.GetFlags()&uint32(signalpb.AttachmentPointer_GIF) != 0 {
content.Info.MauGIF = true
@ -597,8 +590,10 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
},
}
}
content.Body = fileName
content.Info.MimeType = mimeType
if content.Body == "" {
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(content.Info.MimeType)
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
@ -606,203 +601,3 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
Extra: extra,
}, nil
}
func (mc *MessageConverter) actuallyReuploadAttachment(
ctx context.Context,
content *event.MessageEventContent,
att *signalpb.AttachmentPointer,
attMap AttachmentMap,
) (err error) {
convertVoice := att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported()
requireFile := convertVoice
content.URL, content.File, err = getIntent(ctx).UploadMediaStream(ctx, getPortal(ctx).MXID, int64(att.GetSize()), requireFile, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
osFile, ok := file.(*os.File)
inMemData, err := mc.downloadAttachment(ctx, att, attMap, osFile)
if err != nil {
return nil, err
} else if !ok {
if content.Info.MimeType == "" {
content.Info.MimeType = http.DetectContentType(inMemData)
}
_, err = file.Write(inMemData)
return &bridgev2.FileStreamResult{
FileName: content.Body,
MimeType: content.Info.MimeType,
}, err
}
if content.Info.MimeType == "" {
header := make([]byte, 512)
_, err = osFile.ReadAt(header, 0)
if err != nil {
return nil, fmt.Errorf("failed to read file header for MIME type detection: %w", err)
} else {
content.Info.MimeType = http.DetectContentType(header)
}
}
var replFile string
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
replFile, err = ffmpeg.ConvertPath(ctx, osFile.Name(), ".ogg", []string{}, []string{"-c:a", "libopus"}, true)
if err != nil {
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
}
if content.Body == "" {
content.Body = "Voice message.ogg"
} else {
content.Body += ".ogg"
}
content.Info.MimeType = "audio/ogg"
content.MSC3245Voice = &event.MSC3245Voice{}
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
//content.MSC1767Audio = &event.MSC1767Audio{}
}
return &bridgev2.FileStreamResult{
ReplacementFile: replFile,
FileName: content.Body,
MimeType: content.Info.MimeType,
}, nil
})
return
}
func (mc *MessageConverter) convertPollCreateToMatrix(create *signalpb.DataMessage_PollCreate) *bridgev2.ConvertedMessagePart {
evtType := event.EventMessage
if mc.ExtEvPolls {
evtType = event.EventUnstablePollStart
}
maxChoices := 1
if create.GetAllowMultiple() {
maxChoices = len(create.GetOptions())
}
msc3381Answers := make([]map[string]any, len(create.GetOptions()))
optionsListText := make([]string, len(create.GetOptions()))
optionsListHTML := make([]string, len(create.GetOptions()))
for i, option := range create.GetOptions() {
msc3381Answers[i] = map[string]any{
"id": strconv.Itoa(i),
"org.matrix.msc1767.text": option,
}
optionsListText[i] = fmt.Sprintf("%d. %s\n", i+1, option)
optionsListHTML[i] = fmt.Sprintf("<li>%s</li>", event.TextToHTML(option))
}
body := fmt.Sprintf("%s\n\n%s\n\n(This message is a poll. Please open Signal to vote.)", create.GetQuestion(), strings.Join(optionsListText, "\n"))
formattedBody := fmt.Sprintf("<p>%s</p><ol>%s</ol><p>(This message is a poll. Please open Signal to vote.)</p>", event.TextToHTML(create.GetQuestion()), strings.Join(optionsListHTML, ""))
return &bridgev2.ConvertedMessagePart{
Type: evtType,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: body,
Format: event.FormatHTML,
FormattedBody: formattedBody,
},
Extra: map[string]any{
"fi.mau.signal.poll": map[string]any{
"question": create.GetQuestion(),
"allow_multiple": create.GetAllowMultiple(),
"options": create.GetOptions(),
},
"org.matrix.msc1767.message": []map[string]any{
{"mimetype": "text/html", "body": formattedBody},
{"mimetype": "text/plain", "body": body},
},
"org.matrix.msc3381.poll.start": map[string]any{
"kind": "org.matrix.msc3381.poll.disclosed",
"max_selections": maxChoices,
"question": map[string]any{
"org.matrix.msc1767.text": create.GetQuestion(),
},
"answers": msc3381Answers,
},
},
DBMetadata: nil,
DontBridge: false,
}
}
func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, senderACI uuid.UUID, terminate *signalpb.DataMessage_PollTerminate) *bridgev2.ConvertedMessagePart {
pollMessageID := signalid.MakeMessageID(senderACI, terminate.GetTargetSentTimestamp())
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll terminate target message")
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollEnd,
Content: &event.MessageEventContent{},
DontBridge: true,
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollEnd,
Content: &event.MessageEventContent{
RelatesTo: &event.RelatesTo{
Type: event.RelReference,
EventID: pollMessage.MXID,
},
},
Extra: map[string]any{
"org.matrix.msc3381.poll.end": map[string]any{},
},
}
}
var invalidPollVote = &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollResponse,
Content: &event.MessageEventContent{},
DontBridge: true,
}
func (mc *MessageConverter) convertPollVoteToMatrix(ctx context.Context, senderACI uuid.UUID, vote *signalpb.DataMessage_PollVote) *bridgev2.ConvertedMessagePart {
if len(vote.GetTargetAuthorAciBinary()) != 16 {
zerolog.Ctx(ctx).Debug().
Str("author_aci_b64", base64.StdEncoding.EncodeToString(vote.GetTargetAuthorAciBinary())).
Msg("Invalid author ACI in poll vote")
return invalidPollVote
}
pollMessageID := signalid.MakeMessageID(uuid.UUID(vote.GetTargetAuthorAciBinary()), vote.GetTargetSentTimestamp())
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll vote target message")
return invalidPollVote
} else if pollMessage == nil {
zerolog.Ctx(ctx).Warn().Msg("Poll vote target message not found")
return invalidPollVote
}
meta := pollMessage.Metadata.(*signalid.MessageMetadata)
if prevCount, ok := meta.VoteCount[senderACI.String()]; ok && vote.GetVoteCount() <= prevCount {
zerolog.Ctx(ctx).Debug().
Stringer("sender_aci", senderACI).
Uint32("vote_count", vote.GetVoteCount()).
Uint32("previous_vote_count", prevCount).
Msg("Ignoring poll vote with lower vote count")
return invalidPollVote
}
if meta.VoteCount == nil {
meta.VoteCount = make(map[string]uint32)
}
meta.VoteCount[senderACI.String()] = vote.GetVoteCount()
err = mc.Bridge.DB.Message.Update(ctx, pollMessage)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to update poll message with new vote count")
}
mxOptionIDs := meta.MatrixPollOptionIDs
optionIDs := make([]string, len(vote.GetOptionIndexes()))
for i, optionIndex := range vote.GetOptionIndexes() {
if int(optionIndex) < len(mxOptionIDs) {
optionIDs[i] = mxOptionIDs[optionIndex]
} else {
optionIDs[i] = strconv.Itoa(int(optionIndex))
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollResponse,
Content: &event.MessageEventContent{
RelatesTo: &event.RelatesTo{
Type: event.RelReference,
EventID: pollMessage.MXID,
},
},
Extra: map[string]any{
"org.matrix.msc3381.poll.response": map[string]any{
"answers": optionIDs,
},
},
}
}

View file

@ -1,199 +0,0 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package msgconv
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"
"go.mau.fi/util/emojishortcodes"
"google.golang.org/protobuf/proto"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
)
const StickerSourceID = "signal"
const PackURLFormat = "https://signal.art/addstickers/#pack_id=%x&pack_key=%x"
const PackIDLength = 16
const PackKeyLength = 32
const PackURLLength = len(PackURLFormat) - len("%x")*2 + PackIDLength*2 + PackKeyLength*2
var zeroPackID = make([]byte, PackIDLength)
func ParseStickerMeta(info *event.BridgedSticker) *signalpb.DataMessage_Sticker {
if info == nil || info.Network != StickerSourceID || len(info.PackURL) != PackURLLength {
return nil
}
stickerID, err := strconv.ParseUint(info.ID, 10, 32)
if err != nil {
return nil
}
packID, packKey, err := parsePackURL(info.PackURL)
if err != nil || len(packID) != PackIDLength || len(packKey) != PackKeyLength || bytes.Equal(packID, zeroPackID) {
return nil
}
return &signalpb.DataMessage_Sticker{
PackId: packID,
PackKey: packKey,
StickerId: proto.Uint32(uint32(stickerID)),
Emoji: &info.Emoji,
}
}
func parsePackURL(rawURL string) (packID, packKey []byte, err error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, nil, fmt.Errorf("invalid URL: %w", err)
} else if parsed.Host != "signal.art" || !strings.HasPrefix(parsed.Path, "/addstickers") {
return nil, nil, fmt.Errorf("invalid host or path in URL")
}
q, err := url.ParseQuery(parsed.Fragment)
if err != nil {
return nil, nil, fmt.Errorf("invalid URL fragment: %w", err)
}
packID, err = hex.DecodeString(q.Get("pack_id"))
if err != nil {
return nil, nil, fmt.Errorf("invalid pack ID in URL: %w", err)
}
packKey, err = hex.DecodeString(q.Get("pack_key"))
if err != nil {
return nil, nil, fmt.Errorf("invalid pack key in URL: %w", err)
}
return
}
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
packID, packKey, err := parsePackURL(url)
if err != nil {
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
}
manifest, err := signalmeow.DownloadStickerPackManifest(ctx, packID, packKey)
if err != nil {
return nil, fmt.Errorf("failed to download sticker pack manifest: %w", err)
}
topLevelExtra := map[string]any{
"fi.mau.signal.stickerpack": map[string]any{
"pack_id": hex.EncodeToString(packID),
"pack_key": hex.EncodeToString(packKey),
},
}
content := &event.ImagePackEventContent{
Images: make(map[string]*event.ImagePackImage, len(manifest.Stickers)),
Metadata: event.ImagePackMetadata{
DisplayName: manifest.GetTitle(),
AvatarURL: "",
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
Attribution: manifest.GetAuthor(),
BridgedPack: &event.BridgedStickerPack{
Network: StickerSourceID,
URL: fmt.Sprintf(PackURLFormat, packID, packKey),
},
},
}
imagesByID := make(map[uint32]id.ContentURIString, len(manifest.Stickers))
uploadImage := func(sticker *signalpb.Pack_Sticker) (id.ContentURIString, error) {
stickerID := sticker.GetId()
existing, ok := imagesByID[stickerID]
if ok {
return existing, nil
}
var mxc id.ContentURIString
if mc.DirectMedia {
mediaID, err := signalid.DirectMediaSticker{
PackID: packID,
PackKey: packKey,
StickerID: stickerID,
}.AsMediaID()
if err != nil {
return "", fmt.Errorf("failed to create media ID for sticker %d: %w", stickerID, err)
}
mxc, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
if err != nil {
return "", fmt.Errorf("failed to generate content URI for sticker %d: %w", stickerID, err)
}
} else {
dbKey := database.Key(fmt.Sprintf("stickercache:%x:%d", packID, stickerID))
if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
mxc = id.ContentURIString(cached)
imagesByID[stickerID] = mxc
return mxc, nil
}
data, err := signalmeow.DownloadStickerPackItem(ctx, packID, packKey, stickerID)
if err != nil {
return "", fmt.Errorf("failed to download sticker %d: %w", stickerID, err)
}
mxc, _, err = mc.Bridge.Bot.UploadMedia(ctx, "", data, "", sticker.GetContentType())
if err != nil {
return "", fmt.Errorf("failed to upload sticker %d: %w", stickerID, err)
}
mc.Bridge.DB.KV.Set(ctx, dbKey, string(mxc))
}
imagesByID[stickerID] = mxc
return mxc, nil
}
for _, sticker := range manifest.Stickers {
mxc, err := uploadImage(sticker)
if err != nil {
return nil, err
}
shortcode := emojishortcodes.Get(sticker.GetEmoji())
realShortcode := shortcode
i := 2
for _, alreadyExists := content.Images[realShortcode]; alreadyExists; i++ {
realShortcode = fmt.Sprintf("%s_%d", shortcode, i)
}
content.Images[realShortcode] = &event.ImagePackImage{
URL: mxc,
Body: sticker.GetEmoji(),
Info: &event.FileInfo{
MimeType: sticker.GetContentType(),
Width: 200,
Height: 200,
BridgedSticker: &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatUint(uint64(sticker.GetId()), 10),
Emoji: sticker.GetEmoji(),
PackURL: content.Metadata.BridgedPack.URL,
},
},
}
}
if manifest.Cover != nil {
content.Metadata.AvatarURL, err = uploadImage(manifest.Cover)
if err != nil {
return nil, fmt.Errorf("failed to upload sticker pack cover: %w", err)
}
}
return &bridgev2.ImportedImagePack{
Content: content,
Extra: topLevelExtra,
Shortcode: hex.EncodeToString(packID),
}, nil
}

View file

@ -111,13 +111,22 @@ func TestParse_HTML(t *testing.T) {
{
name: "List",
in: "<ul><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ul>",
out: "* woof\n* meow\n* ```\n hmm\n meow\n ```\n* > meow\n > \n > # meow",
out: "* woof\n* meow\n* hmm\n meow\n* > meow\n > \n > # meow",
ent: signalfmt.BodyRangeList{{
Start: 9,
Length: 4,
Value: signalfmt.StyleBold,
}, {
Start: 57,
Start: 16,
Length: 3,
Value: signalfmt.StyleMonospace,
}, {
// FIXME optimally this would be a single range with the previous one so the indent is also monospace
Start: 22,
Length: 4,
Value: signalfmt.StyleMonospace,
}, {
Start: 45,
Length: 6,
Value: signalfmt.StyleBold,
}},
@ -125,13 +134,21 @@ func TestParse_HTML(t *testing.T) {
{
name: "OrderedList",
in: "<ol start=9><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ol>",
out: "9. woof\n10. meow\n11. ```\n hmm\n meow\n ```\n12. > meow\n > \n > # meow",
out: "9. woof\n10. meow\n11. hmm\n meow\n12. > meow\n > \n > # meow",
ent: signalfmt.BodyRangeList{{
Start: 13,
Length: 4,
Value: signalfmt.StyleBold,
}, {
Start: 75,
Start: 22,
Length: 3,
Value: signalfmt.StyleMonospace,
}, {
Start: 30,
Length: 4,
Value: signalfmt.StyleMonospace,
}, {
Start: 59,
Length: 6,
Value: signalfmt.StyleBold,
}},

View file

@ -404,17 +404,17 @@ func (parser *HTMLParser) tagToString(node *html.Node, ctx Context) *EntityStrin
return NewEntityString("---")
case "pre":
var preStr *EntityString
var language string
//var language string
if node.FirstChild != nil && node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" {
class := parser.getAttribute(node.FirstChild, "class")
if strings.HasPrefix(class, "language-") {
language = class[len("language-"):]
}
//class := parser.getAttribute(node.FirstChild, "class")
//if strings.HasPrefix(class, "language-") {
// language = class[len("language-"):]
//}
preStr = parser.nodeToString(node.FirstChild.FirstChild, ctx.WithWhitespace())
} else {
preStr = parser.nodeToString(node.FirstChild, ctx.WithWhitespace())
}
return NewEntityString(fmt.Sprintf("```%s\n", language)).Append(preStr).AppendString("\n```")
return preStr.Format(signalfmt.StyleMonospace)
default:
return parser.nodeToTagAwareString(node.FirstChild, ctx)
}

View file

@ -48,7 +48,6 @@ type MessageConverter struct {
LocationFormat string
DisappearViewOnce bool
DirectMedia bool
ExtEvPolls bool
}
func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
@ -78,7 +77,7 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
parsed, ok := br.Matrix.ParseGhostMXID(userID)
if ok {
u, _ := signalid.ParseUserID(parsed)
u, _ := uuid.Parse(string(parsed))
return u
}
user, _ := br.GetExistingUserByMXID(ctx, userID)
@ -86,7 +85,7 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
if user != nil {
preferredLogin, _, _ := getPortal(ctx).FindPreferredLogin(ctx, user, true)
if preferredLogin != nil {
u, _ := signalid.ParseUserLoginID(preferredLogin.ID)
u, _ := uuid.Parse(string(preferredLogin.ID))
return u
}
}

View file

@ -23,7 +23,6 @@ import (
"strings"
"github.com/google/uuid"
"github.com/rs/zerolog"
"golang.org/x/exp/maps"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -86,27 +85,15 @@ func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, pa
Start: int(*r.Start),
Length: int(*r.Length),
}.TruncateEnd(maxLength)
var mentionACI uuid.UUID
switch rv := r.GetAssociatedValue().(type) {
case *signalpb.BodyRange_Style_:
br.Value = Style(rv.Style)
case *signalpb.BodyRange_MentionAci:
var err error
mentionACI, err = uuid.Parse(rv.MentionAci)
parsed, err := uuid.Parse(rv.MentionAci)
if err != nil {
continue
}
case *signalpb.BodyRange_MentionAciBinary:
if len(rv.MentionAciBinary) != 16 {
continue
}
mentionACI = uuid.UUID(rv.MentionAciBinary)
default:
zerolog.Ctx(ctx).Warn().Type("value_type", rv).Msg("Unsupported body range type")
continue
}
if mentionACI != uuid.Nil {
userInfo := params.GetUserInfo(ctx, mentionACI)
userInfo := params.GetUserInfo(ctx, parsed)
if userInfo.MXID == "" {
continue
}
@ -115,7 +102,7 @@ func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, pa
// Maybe use NewUTF16String and do index replacements for the plaintext body too,
// or just replace the plaintext body by parsing the generated HTML.
content.Body = strings.Replace(content.Body, "\uFFFC", userInfo.Name, 1)
br.Value = Mention{UserInfo: userInfo, UUID: mentionACI}
br.Value = Mention{UserInfo: userInfo, UUID: parsed}
}
lrt.Add(br)
}

View file

@ -40,8 +40,8 @@ func (m Mention) String() string {
}
func (m Mention) Proto() signalpb.BodyRangeAssociatedValue {
return &signalpb.BodyRange_MentionAciBinary{
MentionAciBinary: m.UUID[:],
return &signalpb.BodyRange_MentionAci{
MentionAci: m.UUID.String(),
}
}

View file

@ -29,13 +29,10 @@ type PortalMetadata struct {
type MessageMetadata struct {
ContainsAttachments bool `json:"contains_attachments,omitempty"`
MatrixPollOptionIDs []string `json:"matrix_poll_option_ids,omitempty"`
VoteCount map[string]uint32 `json:"vote_count,omitempty"`
}
type UserLoginMetadata struct {
ChatsSynced bool `json:"chats_synced,omitempty"`
LastContactSync jsontime.UnixMilli `json:"last_contact_sync,omitempty"`
}
type GhostMetadata struct {

View file

@ -34,7 +34,6 @@ const (
directMediaTypeGroupAvatar directMediaType = 1
directMediaTypeProfileAvatar directMediaType = 2
directMediaTypePlaintextDigestAttachment directMediaType = 3
directMediaTypeSticker directMediaType = 4
)
type DirectMediaInfo interface {
@ -45,7 +44,6 @@ var (
_ DirectMediaInfo = (*DirectMediaAttachment)(nil)
_ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaSticker)(nil)
)
type DirectMediaAttachment struct {
@ -129,30 +127,6 @@ func (m DirectMediaProfileAvatar) AsMediaID() (mediaID networkid.MediaID, err er
return networkid.MediaID(buf.Bytes()), nil
}
type DirectMediaSticker struct {
PackID []byte
PackKey []byte
StickerID uint32
}
const packIDLen = 16
const packKeyLen = 32
const directMediaStickerLen = 1 + packIDLen + packKeyLen + 4
func (m DirectMediaSticker) AsMediaID() (mediaID networkid.MediaID, err error) {
if len(m.PackID) != packIDLen {
return nil, fmt.Errorf("invalid pack ID length: %d", len(m.PackID))
} else if len(m.PackKey) != packKeyLen {
return nil, fmt.Errorf("invalid pack key length: %d", len(m.PackKey))
}
mediaID = make(networkid.MediaID, directMediaStickerLen)
mediaID[0] = byte(directMediaTypeSticker)
copy(mediaID[1:], m.PackID)
copy(mediaID[1+packIDLen:], m.PackKey)
binary.BigEndian.PutUint32(mediaID[1+packIDLen+packKeyLen:], m.StickerID)
return mediaID, nil
}
func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err error) {
mediaIDLen := len(mediaID)
if mediaIDLen == 0 {
@ -226,15 +200,6 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err err
info.ProfileAvatarPath = string(profileAvatarPath)
}
return &info, nil
case directMediaTypeSticker:
var info DirectMediaSticker
if len(mediaID) != directMediaStickerLen {
return info, fmt.Errorf("invalid media ID length for sticker: %d", len(mediaID))
}
info.PackID = mediaID[1 : 1+packIDLen]
info.PackKey = mediaID[1+packIDLen : 1+packIDLen+packKeyLen]
info.StickerID = binary.BigEndian.Uint32(mediaID[1+packIDLen+packKeyLen:])
return &info, nil
}
return nil, fmt.Errorf("invalid direct media type %d", mediaType)

View file

@ -31,11 +31,8 @@ import (
"math"
"mime/multipart"
"net/http"
"os"
"github.com/rs/zerolog"
"go.mau.fi/util/fallocate"
"go.mau.fi/util/pkcs7"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
@ -62,54 +59,26 @@ var ErrInvalidMACForAttachment = errors.New("invalid MAC for attachment")
var ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment")
var ErrAttachmentNotFound = errors.New("attachment not found on server")
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte, into *os.File) ([]byte, error) {
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte) ([]byte, error) {
digest := a.GetDigest()
plaintextDigest := false
if digest == nil && plaintextHash != nil {
digest = plaintextHash
plaintextDigest = true
}
return DownloadAttachment(
ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize(), into,
)
return DownloadAttachment(ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize())
}
func DownloadAttachment(
ctx context.Context,
cdnID uint64,
cdnKey string,
cdnNumber uint32,
key, digest []byte,
plaintextDigest bool,
size uint32,
into *os.File,
) ([]byte, error) {
resp, err := web.GetAttachment(ctx, getAttachmentPath(cdnID, cdnKey), cdnNumber)
func DownloadAttachment(ctx context.Context, cdnID uint64, cdnKey string, cdnNumber uint32, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
path := getAttachmentPath(cdnID, cdnKey)
resp, err := web.GetAttachment(ctx, path, cdnNumber, nil)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
bodyReader := resp.Body
defer bodyReader.Close()
var body []byte
var downloadedSize int64
if resp.StatusCode > 400 {
body, err = io.ReadAll(io.LimitReader(resp.Body, 4096))
} else if into == nil {
if resp.ContentLength > 0 {
body = make([]byte, resp.ContentLength)
_, err = io.ReadFull(resp.Body, body)
} else {
body, err = io.ReadAll(http.MaxBytesReader(nil, resp.Body, max(int64(size), 32*1024)*2))
}
} else {
err = fallocate.Fallocate(into, int(resp.ContentLength))
if err != nil {
return nil, fmt.Errorf("failed to pre-allocate file for attachment: %w", err)
}
downloadedSize, err = io.Copy(into, resp.Body)
}
body, err := io.ReadAll(bodyReader)
if err != nil {
return nil, err
}
@ -125,27 +94,12 @@ func DownloadAttachment(
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
if into != nil {
if _, err = into.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek attachment file after downloading: %w", err)
}
return nil, decryptAttachmentFile(into, downloadedSize, key, digest, plaintextDigest, size)
}
return decryptAttachment(body, key, digest, plaintextDigest, size)
}
const MACLength = 32
const IVLength = 16
func macAndAESDecrypt(body, key []byte) ([]byte, error) {
l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
return aesDecrypt(key[:MACLength], body[:l])
}
func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
if !plaintextDigest {
hash := sha256.Sum256(body)
@ -153,7 +107,12 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return nil, ErrInvalidDigestForAttachment
}
}
decrypted, err := macAndAESDecrypt(body, key)
l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
decrypted, err := aesDecrypt(key[:MACLength], body[:l])
if err != nil {
return nil, err
}
@ -170,59 +129,6 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return decrypted, nil
}
func decryptAttachmentFile(file *os.File, downloadedSize int64, key, digest []byte, plaintextDigest bool, size uint32) error {
if !plaintextDigest {
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return ErrInvalidDigestForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing: %w", err)
}
}
mac := make([]byte, MACLength)
n, err := file.ReadAt(mac, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to read MAC from attachment file: %w", err)
} else if n != MACLength {
return fmt.Errorf("unexpected MAC length read from attachment file: %d", n)
}
hasher := hmac.New(sha256.New, key[MACLength:])
_, err = io.CopyN(hasher, file, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to hash attachment file for MAC verification: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), mac) {
return ErrInvalidMACForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after verifying mac: %w", err)
}
decryptedSize, err := aesDecryptFile(key[:MACLength], file, downloadedSize-MACLength)
if err != nil {
return err
} else if decryptedSize < int64(size) {
return fmt.Errorf("decrypted attachment length %d < expected %d", decryptedSize, size)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after decrypting: %w", err)
}
err = file.Truncate(int64(size))
if err != nil {
return fmt.Errorf("failed to truncate attachment file to expected size: %w", err)
}
if plaintextDigest {
hasher = sha256.New()
if _, err = io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash decrypted attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return fmt.Errorf("%w (plaintext hash)", ErrInvalidDigestForAttachment)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing plaintext: %w", err)
}
}
return nil
}
type attachmentV4UploadAttributes struct {
Cdn uint32 `json:"cdn"`
Key string `json:"key"`
@ -245,14 +151,6 @@ func extend(data []byte, paddedLen int) []byte {
}
}
func macAndAESEncrypt(keys, plaintext []byte) ([]byte, error) {
encrypted, err := aesEncrypt(keys[:32], plaintext)
if err != nil {
return nil, err
}
return appendMAC(keys[32:], encrypted), nil
}
func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
keys := random.Bytes(64) // combined AES and MAC keys
@ -268,20 +166,23 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
}
body = extend(body, paddedLen)
encryptedWithMAC, err := macAndAESEncrypt(keys, body)
encrypted, err := aesEncrypt(keys[:32], body)
if err != nil {
return nil, err
}
encryptedWithMAC := appendMAC(keys[32:], encrypted)
// Get upload attributes from Signal server
attributesPath := "/v4/attachments/form/upload"
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil)
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, attributesPath, opts)
if err != nil {
log.Err(err).Msg("Failed to request upload attributes")
return nil, fmt.Errorf("failed to request upload attributes: %w", err)
}
var uploadAttributes attachmentV4UploadAttributes
err = web.DecodeWSResponseBody(ctx, &uploadAttributes, resp)
err = web.DecodeHTTPResponseBody(ctx, &uploadAttributes, resp)
if err != nil {
log.Err(err).Msg("Failed to decode upload attributes")
return nil, fmt.Errorf("failed to decode upload attributes: %w", err)
@ -291,7 +192,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC)
} else {
log.Trace().Msg("Using legacy upload")
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC)
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC, username, password)
}
if err != nil {
log.Err(err).Msg("Failed to upload attachment")
@ -317,17 +218,17 @@ func (cli *Client) uploadAttachmentLegacy(
ctx context.Context,
uploadAttributes attachmentV4UploadAttributes,
encryptedWithMAC []byte,
username string,
password string,
) error {
username, password := cli.Store.BasicAuthCreds()
// Allocate attachment on CDN
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation,
ContentType: web.ContentTypeOctetStream,
Headers: uploadAttributes.Headers,
Username: &username,
Password: &password,
})
web.CloseBody(resp)
if err != nil {
return fmt.Errorf("failed to send allocate request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -335,14 +236,13 @@ func (cli *Client) uploadAttachmentLegacy(
}
// Upload attachment to CDN
resp, err = web.SendHTTPRequest(ctx, "", http.MethodPut, "", &web.HTTPReqOpt{
resp, err = web.SendHTTPRequest(ctx, http.MethodPut, "", &web.HTTPReqOpt{
OverrideURL: resp.Header.Get("Location"),
Body: encryptedWithMAC,
ContentType: web.ContentTypeOctetStream,
Username: &username,
Password: &password,
})
web.CloseBody(resp)
if err != nil {
return fmt.Errorf("failed to send upload request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -360,13 +260,12 @@ func (cli *Client) uploadAttachmentTUS(
uploadAttributes.Headers["Upload-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC))
uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key))
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation,
Body: encryptedWithMAC,
ContentType: web.ContentTypeOffsetOctetStream,
Headers: uploadAttributes.Headers,
})
web.CloseBody(resp)
// TODO actually support resuming on error
if err != nil {
return fmt.Errorf("failed to send upload request: %w", err)
@ -381,17 +280,12 @@ func (cli *Client) uploadAttachmentTUS(
return nil
}
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier, groupMasterKey types.SerializedGroupMasterKey) (string, error) {
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier) (string, error) {
log := zerolog.Ctx(ctx)
if groupMasterKey == "" {
var err error
groupMasterKey, err = cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
log.Err(err).Msg("Could not get master key from group id")
return "", err
} else if groupMasterKey == "" {
return "", fmt.Errorf("no master key found for group %s", gid)
}
}
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil {
@ -411,15 +305,14 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
}
// Get upload form from Signal server
formPath := "/v2/groups/avatar/form"
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf}
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, formPath, opts)
formPath := "/v1/groups/avatar/form"
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf, Host: web.StorageHostname}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, formPath, opts)
if err != nil {
log.Err(err).Msg("Error sending request fetching avatar upload form")
return "", err
}
body, err := io.ReadAll(resp.Body)
web.CloseBody(resp)
if err != nil {
log.Err(err).Msg("Error decoding response body fetching upload attributes")
return "", err
@ -445,11 +338,11 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
w.Close()
// Upload avatar to CDN
resp, err = web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodPost, "", &web.HTTPReqOpt{
resp, err = web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
Body: requestBody.Bytes(),
ContentType: web.ContentType(w.FormDataContentType()),
Host: web.CDN1Hostname,
})
web.CloseBody(resp)
if err != nil {
log.Err(err).Msg("Error sending request uploading attachment")
return "", err
@ -478,56 +371,14 @@ func aesDecrypt(key, ciphertext []byte) ([]byte, error) {
return nil, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", len(ciphertext)%aes.BlockSize)
}
iv := ciphertext[:IVLength]
ciphertext = ciphertext[IVLength:]
iv := ciphertext[:aes.BlockSize]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext)
}
func aesDecryptFile(key []byte, file *os.File, downloadedSize int64) (int64, error) {
block, err := aes.NewCipher(key)
if err != nil {
return 0, err
}
fileReader := io.LimitReader(file, downloadedSize)
if downloadedSize%aes.BlockSize != 0 {
return 0, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", downloadedSize%aes.BlockSize)
}
iv := make([]byte, IVLength)
n, err := fileReader.Read(iv)
if err != nil {
return 0, fmt.Errorf("failed to read IV from attachment file: %w", err)
} else if n != IVLength {
return 0, fmt.Errorf("unexpected IV length read from attachment file: %d", n)
}
mode := cipher.NewCBCDecrypter(block, iv)
buf := make([]byte, 4096)
var offset int64
var pad byte
for {
n, err = fileReader.Read(buf)
if err != nil && !errors.Is(err, io.EOF) {
return 0, fmt.Errorf("failed to read from attachment file: %w", err)
}
if n > 0 {
mode.CryptBlocks(buf[:n], buf[:n])
if _, err = file.WriteAt(buf[:n], offset); err != nil {
return 0, fmt.Errorf("failed to write decrypted data to attachment file: %w", err)
}
offset += int64(n)
pad = buf[n-1]
}
if errors.Is(err, io.EOF) {
break
}
}
pad := ciphertext[len(ciphertext)-1]
if pad > aes.BlockSize {
return 0, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
return nil, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
}
return downloadedSize - int64(pad), nil
return ciphertext[aes.BlockSize : len(ciphertext)-int(pad)], nil
}
func appendMAC(key, body []byte) []byte {
@ -542,11 +393,14 @@ func aesEncrypt(key, plaintext []byte) ([]byte, error) {
return nil, err
}
plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
pad := aes.BlockSize - len(plaintext)%aes.BlockSize
plaintext = append(plaintext, bytes.Repeat([]byte{byte(pad)}, pad)...)
ciphertext := make([]byte, len(plaintext))
iv := random.Bytes(16)
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(plaintext, plaintext)
mode.CryptBlocks(ciphertext, plaintext)
return append(iv, plaintext...), nil
return append(iv, ciphertext...), nil
}

View file

@ -239,7 +239,7 @@ func (cli *Client) deriveTransferKeys() (aesKey, hmacKey [32]byte, err error) {
}
func downloadTransferArchive(ctx context.Context, meta *TransferArchiveMetadata, writeTo io.Writer) error {
resp, err := web.GetAttachment(ctx, getAttachmentPath(0, meta.Key), meta.CDN)
resp, err := web.GetAttachment(ctx, getAttachmentPath(0, meta.Key), meta.CDN, nil)
if err != nil {
return fmt.Errorf("failed to download transfer archive: %w", err)
}
@ -282,11 +282,7 @@ func (cli *Client) WaitForTransfer(ctx context.Context) (*TransferArchiveMetadat
}
reqDuration := time.Since(reqStart)
if reqDuration < reqTimeout-10*time.Second {
select {
case <-time.After(15 * time.Second):
case <-ctx.Done():
return nil, ctx.Err()
}
time.Sleep(15 * time.Second)
}
}
}
@ -295,14 +291,21 @@ func (cli *Client) tryRequestTransferArchive(ctx context.Context, timeout time.D
reqCtx, cancel := context.WithTimeout(ctx, timeout+15*time.Second)
defer cancel()
path := "/v1/devices/transfer_archive?timeout=" + strconv.Itoa(int(timeout.Seconds()))
resp, err := cli.AuthedWS.SendRequest(reqCtx, http.MethodGet, path, nil, nil)
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(reqCtx, http.MethodGet, path, opts)
defer func() {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}()
if err != nil {
return nil, err
} else if resp.GetStatus() == http.StatusNoContent {
} else if resp.StatusCode == http.StatusNoContent {
return nil, nil
} else if resp.GetStatus() != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.GetStatus())
} else if err = json.Unmarshal(resp.Body, &respBody); err != nil {
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
} else if err = json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
} else {
return respBody, nil

View file

@ -18,21 +18,16 @@ package signalmeow
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"sync"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
@ -40,14 +35,12 @@ type Client struct {
Store *store.Device
Log zerolog.Logger
senderCertificateWithE164 *libsignalgo.SenderCertificate
senderCertificateNoE164 *libsignalgo.SenderCertificate
senderCertificateCache sync.Mutex
sendCache *exsync.RingBuffer[sendCacheKey, *signalpb.Content]
SenderCertificateWithE164 *libsignalgo.SenderCertificate
SenderCertificateNoE164 *libsignalgo.SenderCertificate
GroupCredentials *GroupCredentials
GroupCache *GroupCache
ProfileCache *ProfileCache
GroupCallCache *map[string]bool
LastContactRequestTime time.Time
SyncContactsOnConnect bool
@ -71,26 +64,6 @@ type Client struct {
writeCallbackCounter chan time.Time
}
// InMemorySendCacheSize specifies how large the cache for sent messages is, which is used to respond to retry receipts.
// The cache is large because every group member will be listed separately.
// 2k entries should hold at least 2 messages in max size groups.
var InMemorySendCacheSize = 2048
func NewClient(device *store.Device, log zerolog.Logger, evtHandler func(events.SignalEvent) bool) *Client {
return &Client{
Store: device,
Log: log,
EventHandler: evtHandler,
GroupCache: NewGroupCache(device.ACIServiceID()),
ProfileCache: &ProfileCache{
profiles: make(map[string]*types.Profile),
errors: make(map[string]*error),
lastFetched: make(map[string]time.Time),
},
sendCache: exsync.NewRingBuffer[sendCacheKey, *signalpb.Content](InMemorySendCacheSize),
}
}
func (cli *Client) handleEvent(evt events.SignalEvent) bool {
return cli.EventHandler(evt)
}
@ -137,11 +110,3 @@ func (cli *Client) connectUnauthedWS(ctx context.Context) (chan web.SignalWebsoc
func (cli *Client) IsLoggedIn() bool {
return cli.Store != nil && cli.Store.IsDeviceLoggedIn()
}
func (cli *Client) GetRemoteConfig(ctx context.Context) (json.RawMessage, error) {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, "/v2/config", nil, nil)
if err != nil {
return nil, err
}
return resp.Body, web.DecodeWSResponseBody(ctx, nil, resp)
}

View file

@ -36,7 +36,7 @@ import (
)
func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDetails *signalpb.ContactDetails, avatar *[]byte) (*types.Recipient, error) {
parsedUUID, err := ParseStringOrBinaryUUID(contactDetails.GetAci(), contactDetails.GetAciBinary())
parsedUUID, err := uuid.Parse(contactDetails.GetAci())
if err != nil {
return nil, err
}

View file

@ -35,14 +35,12 @@ import (
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/cds2pb"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
// ProdContactDiscoveryMrenclave should always match ENCLAVE_ID_CDSI_PROD from libsignal
// https://github.com/signalapp/libsignal/blob/main/rust/attest/src/constants.rs#L69
const ProdContactDiscoveryMrenclave = "15637fa1e54fe655176d3df1a9f94b87c01ed377acaa570682dc5d72c95ef07b"
const ProdContactDiscoveryServer = "cdsi.signal.org"
const ProdContactDiscoveryMrenclave = "ee9503070127120074612b6688e593b67e486b1541449f54d71e387484eb40a3"
const ContactDiscoveryAuthTTL = 23 * time.Hour
const rateLimitCloseCode = websocket.StatusCode(4008)
@ -83,7 +81,7 @@ func (cli *Client) LookupPhone(ctx context.Context, e164s ...uint64) (ContactDis
}
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
resp, token, err := cli.doContactDiscovery(ctx, &cds2pb.ClientRequest{
resp, token, err := cli.doContactDiscovery(ctx, &signalpb.CDSClientRequest{
// TODO figure out if tokens are useful
// (it's meant for old_e164s)
//Token: cli.cdToken,
@ -95,7 +93,7 @@ func (cli *Client) LookupPhone(ctx context.Context, e164s ...uint64) (ContactDis
return resp, err
}
func (cli *Client) doContactDiscovery(ctx context.Context, req *cds2pb.ClientRequest) (ContactDiscoveryResponse, []byte, error) {
func (cli *Client) doContactDiscovery(ctx context.Context, req *signalpb.CDSClientRequest) (ContactDiscoveryResponse, []byte, error) {
creds, err := cli.getContactDiscoveryCredentials(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch contact discovery auth: %w", err)
@ -187,7 +185,7 @@ func (cdc *ContactDiscoveryClient) Handshake(ctx context.Context) error {
return nil
}
func (cdc *ContactDiscoveryClient) SendRequest(ctx context.Context, req *cds2pb.ClientRequest) error {
func (cdc *ContactDiscoveryClient) SendRequest(ctx context.Context, req *signalpb.CDSClientRequest) error {
plaintext, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
@ -224,15 +222,15 @@ func (cdc *ContactDiscoveryClient) handleResponse(ctx context.Context, msg []byt
if err != nil {
return fmt.Errorf("failed to decrypt message: %w", err)
}
var cdsClientResp cds2pb.ClientResponse
var cdsClientResp signalpb.CDSClientResponse
err = proto.Unmarshal(decrypted, &cdsClientResp)
if err != nil {
return fmt.Errorf("failed to unmarshal message: %w", err)
}
if cdsClientResp.Token != nil {
cdc.Token = cdsClientResp.Token
err = cdc.SendRequest(ctx, &cds2pb.ClientRequest{
TokenAck: true,
err = cdc.SendRequest(ctx, &signalpb.CDSClientRequest{
TokenAck: proto.Bool(true),
})
if err != nil {
return fmt.Errorf("failed to send token ack request: %w", err)

View file

@ -30,6 +30,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
func hmacSHA256(key, input []byte) []byte {
@ -62,12 +63,18 @@ func (cli *Client) updateDeviceName(ctx context.Context, encryptedName []byte) e
if err != nil {
return fmt.Errorf("failed to marshal device name update request: %w", err)
}
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/accounts/name", reqData, nil)
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, "/v1/accounts/name", &web.HTTPReqOpt{
Body: reqData,
Username: &username,
Password: &password,
})
if err != nil {
return fmt.Errorf("failed to send device name update request: %w", err)
}
if resp.GetStatus() < 200 || resp.GetStatus() >= 300 {
return fmt.Errorf("device name update request returned status %d", resp.GetStatus())
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("device name update request returned status %d", resp.StatusCode)
}
return nil
}

View file

@ -36,7 +36,6 @@ func (*Call) isSignalEvent() {}
func (*ContactList) isSignalEvent() {}
func (*ACIFound) isSignalEvent() {}
func (*DeleteForMe) isSignalEvent() {}
func (*MessageRequestResponse) isSignalEvent() {}
func (*QueueEmpty) isSignalEvent() {}
func (*LoggedOut) isSignalEvent() {}
@ -90,14 +89,6 @@ type DeleteForMe struct {
*signalpb.SyncMessage_DeleteForMe
}
type MessageRequestResponse struct {
Timestamp uint64
ThreadACI uuid.UUID
GroupID *libsignalgo.GroupIdentifier
Type signalpb.SyncMessage_MessageRequestResponse_Type
Raw *signalpb.SyncMessage_MessageRequestResponse
}
type QueueEmpty struct{}
type LoggedOut struct{ Error error }

View file

@ -1,346 +0,0 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2025 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package signalmeow
import (
"context"
"fmt"
"slices"
"sync"
"time"
"github.com/google/uuid"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
type SendEndorsementCache struct {
SendEndorsement libsignalgo.GroupSendEndorsement
MemberEndorsements map[libsignalgo.ServiceID]libsignalgo.GroupSendEndorsement
Expiration time.Time
SecretParams *libsignalgo.GroupSecretParams
}
func (sec *SendEndorsementCache) GetToken() (libsignalgo.GroupSendFullToken, error) {
return sec.GetTokenWith(sec.SendEndorsement)
}
func (sec *SendEndorsementCache) GetTokenWith(altToken libsignalgo.GroupSendEndorsement) (libsignalgo.GroupSendFullToken, error) {
return altToken.ToFullToken(sec.SecretParams, sec.Expiration)
}
type cachedGroup struct {
*Group
*SendEndorsementCache
FetchedAt time.Time
UpdatedAt time.Time
}
type GroupCache struct {
serviceID libsignalgo.ServiceID
credentials *GroupCredentials
credentialsLock sync.RWMutex
data map[types.GroupIdentifier]*cachedGroup
lock sync.RWMutex
activeCalls map[types.GroupIdentifier]string
callsLock sync.RWMutex
}
func NewGroupCache(serviceID libsignalgo.ServiceID) *GroupCache {
return &GroupCache{
serviceID: serviceID,
data: make(map[types.GroupIdentifier]*cachedGroup),
activeCalls: make(map[types.GroupIdentifier]string),
}
}
func (gc *GroupCache) GetCredentials(
ctx context.Context,
fetch func(context.Context, time.Time) (*GroupCredentials, error),
) (*GroupCredential, error) {
today := time.Now().Truncate(24 * time.Hour)
gc.credentialsLock.RLock()
cred := gc.getCachedCredentials(today.Unix())
gc.credentialsLock.RUnlock()
if cred != nil {
return cred, nil
}
gc.credentialsLock.Lock()
defer gc.credentialsLock.Unlock()
cred = gc.getCachedCredentials(today.Unix())
if cred != nil {
return cred, nil
}
creds, err := fetch(ctx, today)
if err != nil {
return nil, err
}
gc.credentials = creds
cred = gc.getCachedCredentials(today.Unix())
if cred == nil {
return nil, fmt.Errorf("no credentials for today after fetch")
}
return cred, nil
}
func (gc *GroupCache) getCachedCredentials(today int64) *GroupCredential {
if gc.credentials == nil {
return nil
}
for _, cred := range gc.credentials.Credentials {
if cred.RedemptionTime == today {
return &cred
}
}
return nil
}
func (gc *GroupCache) UpdateActiveCall(id types.GroupIdentifier, callID string) bool {
gc.callsLock.Lock()
defer gc.callsLock.Unlock()
currentCallID, ok := gc.activeCalls[id]
if ok {
// If we do, then this must be ending the call
if currentCallID == callID {
delete(gc.activeCalls, id)
return false
}
}
gc.activeCalls[id] = callID
return true
}
func (gc *GroupCache) Get(id types.GroupIdentifier) (*Group, *SendEndorsementCache, bool) {
gc.lock.RLock()
defer gc.lock.RUnlock()
c, ok := gc.data[id]
if !ok || time.Until(c.Expiration) < 5*time.Minute {
return nil, nil, false
}
return c.Group, c.SendEndorsementCache, true
}
func (gc *GroupCache) Delete(id types.GroupIdentifier) {
gc.lock.Lock()
defer gc.lock.Unlock()
delete(gc.data, id)
}
func (gc *GroupCache) Put(data *Group, endorsementResponse libsignalgo.GroupSendEndorsementsResponse) error {
gsp, err := masterKeyToBytes(data.GroupMasterKey).SecretParams()
if err != nil {
return fmt.Errorf("failed to get secret params: %w", err)
}
expiration, err := endorsementResponse.GetExpiration()
if err != nil {
return fmt.Errorf("failed to get endorsement expiration: %w", err)
}
endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs(data.getMemberServiceIDs(), gc.serviceID, &gsp, prodServerPublicParams)
if err != nil {
return fmt.Errorf("failed to receive endorsements: %w", err)
}
gc.lock.Lock()
defer gc.lock.Unlock()
cached, exists := gc.data[data.GroupIdentifier]
if exists && cached.Revision > data.Revision {
return nil
}
gc.data[data.GroupIdentifier] = &cachedGroup{
Group: data,
FetchedAt: time.Now(),
UpdatedAt: time.Now(),
SendEndorsementCache: &SendEndorsementCache{
Expiration: expiration,
SendEndorsement: endorsement,
MemberEndorsements: memberEndorsements,
SecretParams: &gsp,
},
}
return nil
}
func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsignalgo.GroupSendEndorsementsResponse) error {
mkBytes := masterKeyToBytes(change.GroupMasterKey)
rawGroupID, err := mkBytes.GroupIdentifier()
if err != nil {
return fmt.Errorf("failed to get group identifier: %w", err)
}
gsp, err := mkBytes.SecretParams()
if err != nil {
return fmt.Errorf("failed to get secret params: %w", err)
}
id := types.GroupIdentifier(rawGroupID.String())
gc.lock.Lock()
defer gc.lock.Unlock()
cached, exists := gc.data[id]
if !exists || cached.Revision >= change.Revision {
return nil
} else if cached.Revision < change.Revision-1 {
// We missed an update, evict
delete(gc.data, id)
return nil
}
// Pending member adds, promotes and removes
cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...)
for _, promo := range change.PromotePendingMembers {
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return p.ServiceID.Type == libsignalgo.ServiceIDTypeACI && p.ServiceID.UUID == promo.ACI
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: promo.ProfileKey,
Role: GroupMember_DEFAULT,
JoinedAtRevision: change.Revision,
})
}
for _, promo := range change.PromotePendingPniAciMembers {
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return (p.ServiceID.Type == libsignalgo.ServiceIDTypePNI && p.ServiceID.UUID == promo.PNI) ||
(p.ServiceID.Type == libsignalgo.ServiceIDTypeACI && p.ServiceID.UUID == promo.ACI)
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: promo.ProfileKey,
Role: GroupMember_DEFAULT,
JoinedAtRevision: change.Revision,
})
}
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return slices.ContainsFunc(change.DeletePendingMembers, func(s *libsignalgo.ServiceID) bool {
return s != nil && p.ServiceID == *s
})
})
// Requesting member adds, promotes and removes
cached.RequestingMembers = append(cached.RequestingMembers, change.AddRequestingMembers...)
for _, promo := range change.PromoteRequestingMembers {
var profileKey libsignalgo.ProfileKey
cached.RequestingMembers = slices.DeleteFunc(cached.RequestingMembers, func(r *RequestingMember) bool {
if r.ACI == promo.ACI {
profileKey = r.ProfileKey
return true
}
return false
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: profileKey,
Role: promo.Role,
JoinedAtRevision: change.Revision,
})
}
cached.RequestingMembers = slices.DeleteFunc(cached.RequestingMembers, func(r *RequestingMember) bool {
return slices.ContainsFunc(change.DeleteRequestingMembers, func(u *uuid.UUID) bool {
return u != nil && r.ACI == *u
})
})
// Direct member adds, removes and modifications
for _, member := range change.AddMembers {
cached.Members = append(cached.Members, &GroupMember{
ACI: member.ACI,
Role: member.Role,
ProfileKey: member.ProfileKey,
JoinedAtRevision: member.JoinedAtRevision,
})
}
for _, rm := range change.ModifyMemberRoles {
cached.findMemberOrEmpty(rm.ACI).Role = rm.Role
}
for _, pk := range change.ModifyMemberProfileKeys {
cached.findMemberOrEmpty(pk.ACI).ProfileKey = pk.ProfileKey
}
cached.Members = slices.DeleteFunc(cached.Members, func(member *GroupMember) bool {
return slices.ContainsFunc(change.DeleteMembers, func(u *uuid.UUID) bool {
return u != nil && *u == member.ACI
})
})
// Banned members
cached.BannedMembers = append(cached.BannedMembers, change.AddBannedMembers...)
cached.BannedMembers = slices.DeleteFunc(cached.BannedMembers, func(b *BannedMember) bool {
return slices.ContainsFunc(change.DeleteBannedMembers, func(s *libsignalgo.ServiceID) bool {
return s != nil && b.ServiceID == *s
})
})
// Non-member modifications
if change.ModifyInviteLinkPassword != nil {
cached.InviteLinkPassword = change.ModifyInviteLinkPassword
}
if change.ModifyTitle != nil {
cached.Title = *change.ModifyTitle
}
if change.ModifyDescription != nil {
cached.Description = *change.ModifyDescription
}
if change.ModifyAvatar != nil {
cached.AvatarPath = *change.ModifyAvatar
}
if change.ModifyAnnouncementsOnly != nil {
cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly
}
if change.ModifyDisappearingMessagesDuration != nil {
cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration
}
if change.ModifyAttributesAccess != nil {
cached.AccessControl.Attributes = *change.ModifyAttributesAccess
}
if change.ModifyMemberAccess != nil {
cached.AccessControl.Members = *change.ModifyMemberAccess
}
if change.ModifyAddFromInviteLinkAccess != nil {
cached.AccessControl.AddFromInviteLink = *change.ModifyAddFromInviteLinkAccess
}
cached.UpdatedAt = time.Now()
cached.Revision = change.Revision
endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs(
cached.getMemberServiceIDs(),
gc.serviceID,
&gsp,
prodServerPublicParams,
)
if err != nil {
delete(gc.data, id)
return fmt.Errorf("failed to receive endorsements: %w", err)
}
expiration, err := endorsementResponse.GetExpiration()
if err != nil {
delete(gc.data, id)
return fmt.Errorf("failed to get endorsement expiration: %w", err)
}
// TODO do these responses overwrite the entire thing?
cached.SendEndorsementCache = &SendEndorsementCache{
SendEndorsement: endorsement,
MemberEndorsements: memberEndorsements,
Expiration: expiration,
SecretParams: &gsp,
}
return nil
}

View file

@ -31,7 +31,6 @@ import (
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
@ -91,12 +90,6 @@ type Group struct {
//PublicKey *libsignalgo.PublicKey
}
func (group *Group) getMemberServiceIDs() []libsignalgo.ServiceID {
return exslices.CastFunc(group.Members, func(from *GroupMember) libsignalgo.ServiceID {
return libsignalgo.NewACIServiceID(from.ACI)
})
}
func (group *Group) GetInviteLink() (string, error) {
if group.InviteLinkPassword == nil {
return "", fmt.Errorf("no invite link password set")
@ -106,8 +99,8 @@ func (group *Group) GetInviteLink() (string, error) {
if err != nil {
return "", fmt.Errorf("couldn't decode invite link password")
}
inviteLinkContents := signalpb.GroupInviteLink_ContentsV1{
ContentsV1: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
inviteLinkContents := signalpb.GroupInviteLink_V1Contents{
V1Contents: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
GroupMasterKey: masterKeyBytes[:],
InviteLinkPassword: inviteLinkPasswordBytes,
},
@ -121,15 +114,6 @@ func (group *Group) GetInviteLink() (string, error) {
return "https://signal.group/#" + inviteLinkPath, nil
}
func (group *Group) findMemberOrEmpty(aci uuid.UUID) *GroupMember {
for _, member := range group.Members {
if member.ACI == aci {
return member
}
}
return &GroupMember{}
}
type GroupAccessControl struct {
Members AccessControl
AddFromInviteLink AccessControl
@ -226,7 +210,8 @@ func (groupChange *GroupChange) isEmpty() bool {
len(groupChange.PromoteRequestingMembers) == 0 &&
groupChange.ModifyDescription == nil &&
groupChange.ModifyAnnouncementsOnly == nil &&
len(groupChange.AddBannedMembers) == 0
len(groupChange.AddBannedMembers) == 0 &&
len(groupChange.DeleteMembers) == 0
}
func (groupChange *GroupChange) resolveConflict(group *Group) {
@ -329,7 +314,8 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
Logger()
sevenDaysOut := today.Add(7 * 24 * time.Hour)
path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix())
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, path, nil, nil)
authRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
resp, err := cli.AuthedWS.SendRequest(ctx, authRequest)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
@ -343,22 +329,51 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
log.Err(err).Msg("json.Unmarshal error")
return nil, err
}
// make sure pni matches device pni
if creds.PNI != cli.Store.PNI {
return nil, fmt.Errorf("mismatching PNI in group credentials: %s != %s", creds.PNI, cli.Store.PNI)
err := fmt.Errorf("creds.PNI != d.PNI")
log.Err(err).Msg("creds.PNI != d.PNI")
return nil, err
}
return &creds, nil
}
func (cli *Client) getCachedAuthorizationForToday(today time.Time) *GroupCredential {
if cli.GroupCredentials == nil {
// No cached credentials
return nil
}
allCreds := cli.GroupCredentials
// Get the credential for today
for _, cred := range allCreds.Credentials {
if cred.RedemptionTime == today.Unix() {
return &cred
}
}
return nil
}
func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) {
log := zerolog.Ctx(ctx).With().
Str("action", "get authorization for today").
Logger()
// Timestamps for the start of today, and 7 days later
today := time.Now().Truncate(24 * time.Hour)
todayCred, err := cli.GroupCache.GetCredentials(ctx, cli.fetchNewGroupCreds)
todayCred := cli.getCachedAuthorizationForToday(today)
if todayCred == nil {
creds, err := cli.fetchNewGroupCreds(ctx, today)
if err != nil {
return nil, fmt.Errorf("failed to get group credentials: %w", err)
return nil, fmt.Errorf("fetchNewGroupCreds error: %w", err)
}
cli.GroupCredentials = creds
todayCred = cli.getCachedAuthorizationForToday(today)
}
if todayCred == nil {
return nil, fmt.Errorf("couldn't get credential for today")
}
//TODO: cache cred after unmarshalling
redemptionTime := uint64(todayCred.RedemptionTime)
credential := todayCred.Credential
authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(credential)
@ -470,7 +485,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description)
if err == nil {
// treat a failure in obtaining the description as non-fatal
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescriptionText())
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescription())
}
if encryptedGroup.DisappearingMessagesTimer != nil && len(encryptedGroup.DisappearingMessagesTimer) > 0 {
@ -482,8 +497,8 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
}
// These aren't encrypted
decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl
decryptedGroup.Revision = encryptedGroup.Version
decryptedGroup.AvatarPath = encryptedGroup.Avatar
decryptedGroup.Revision = encryptedGroup.Revision
// Decrypt members
for _, member := range encryptedGroup.Members {
@ -497,7 +512,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember)
}
for _, pendingMember := range encryptedGroup.MembersPendingProfileKey {
for _, pendingMember := range encryptedGroup.PendingMembers {
if pendingMember == nil {
continue
}
@ -509,7 +524,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember)
}
for _, requestingMember := range encryptedGroup.MembersPendingAdminApproval {
for _, requestingMember := range encryptedGroup.RequestingMembers {
if requestingMember == nil {
continue
}
@ -520,7 +535,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember)
}
for _, bannedMember := range encryptedGroup.MembersBanned {
for _, bannedMember := range encryptedGroup.BannedMembers {
if bannedMember == nil {
continue
}
@ -619,7 +634,7 @@ func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier
return nil, fmt.Errorf("failed to get group master key: %w", err)
}
if groupMasterKey == "" {
return nil, fmt.Errorf("%w for %s", ErrGroupMasterKeyNotFound, gid)
return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
}
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
}
@ -634,14 +649,14 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
Username: &groupAuth.Username,
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Host: web.StorageHostname,
}
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, "/v2/groups", opts)
defer web.CloseBody(response)
response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v2/groups", opts)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, fmt.Errorf("unexpected response status: %d", response.StatusCode)
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
}
return cli.parseGroupResponse(ctx, response, groupMasterKey)
}
@ -661,10 +676,6 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
if err != nil {
return nil, fmt.Errorf("failed to decrypt group: %w", err)
}
err = cli.GroupCache.Put(group, groupResponse.GroupSendEndorsementsResponse)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to cache group response")
}
// Store the profile keys in case they're new
for _, member := range group.Members {
@ -685,11 +696,11 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username,
Password: &password,
}
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts)
defer web.CloseBody(resp)
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
@ -708,21 +719,23 @@ func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, g
return decrypted, nil
}
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, *SendEndorsementCache, error) {
cached, endorsement, ok := cli.GroupCache.Get(gid)
if ok && cached.Revision >= revision {
return cached, endorsement, nil
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, error) {
cli.initGroupCache()
lastFetched, ok := cli.GroupCache.lastFetched[gid]
if ok && time.Since(lastFetched) < 1*time.Hour {
group, ok := cli.GroupCache.groups[gid]
if ok && group.Revision >= revision {
return group, nil
}
}
group, err := cli.fetchGroupByID(ctx, gid)
if err != nil {
return nil, nil, err
return nil, err
}
cached, endorsement, ok = cli.GroupCache.Get(gid)
if !ok {
zerolog.Ctx(ctx).Warn().Msg("Group not found in cache after fetching")
return group, nil, nil
}
return cached, endorsement, nil
cli.GroupCache.groups[gid] = group
cli.GroupCache.lastFetched[gid] = time.Now()
return group, nil
}
// We should store the group master key in the group store as soon as we see it,
@ -740,6 +753,40 @@ func (cli *Client) StoreMasterKey(ctx context.Context, groupMasterKey types.Seri
return groupIdentifier, nil
}
// We need to track active calls so we don't send too many IncomingSignalMessageCalls
// Of course for group calls Signal doesn't tell us *anything* so we're mostly just inferring
// So we just jam a new call ID in, and return true if we *think* this is a new incoming call
func (cli *Client) UpdateActiveCalls(gid types.GroupIdentifier, callID string) (isActive bool) {
cli.initGroupCache()
// Check to see if we currently have an active call for this group
currentCallID, ok := cli.GroupCache.activeCalls[gid]
if ok {
// If we do, then this must be ending the call
if currentCallID == callID {
delete(cli.GroupCache.activeCalls, gid)
return false
}
}
cli.GroupCache.activeCalls[gid] = callID
return true
}
func (cli *Client) initGroupCache() {
if cli.GroupCache == nil {
cli.GroupCache = &GroupCache{
groups: make(map[types.GroupIdentifier]*Group),
lastFetched: make(map[types.GroupIdentifier]time.Time),
activeCalls: make(map[types.GroupIdentifier]string),
}
}
}
type GroupCache struct {
groups map[types.GroupIdentifier]*Group
lastFetched map[types.GroupIdentifier]time.Time
activeCalls map[types.GroupIdentifier]string
}
func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) {
masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey)
groupMasterKey := masterKeyFromBytes(masterKeyBytes)
@ -757,15 +804,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger()
serverSignature := encryptedGroupChange.ServerSignature
encryptedActionsBytes := encryptedGroupChange.Actions
var success bool
defer func() {
if !success {
rawGroupID, _ := masterKeyToBytes(groupMasterKey).GroupIdentifier()
if rawGroupID != nil {
cli.GroupCache.Delete(types.GroupIdentifier(rawGroupID.String()))
}
}
}()
var err error
if verifySignature {
@ -788,14 +826,14 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err
}
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceUserId))
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId))
if err != nil {
log.Err(err).Msg("Couldn't decrypt source serviceID")
return nil, err
}
decryptedGroupChange := &GroupChange{
GroupMasterKey: groupMasterKey,
Revision: encryptedActions.Version,
Revision: encryptedActions.Revision,
SourceServiceID: sourceServiceID,
}
@ -815,7 +853,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description)
if err == nil {
// treat a failure in obtaining the description as non-fatal
newDescription := cleanupStringProperty(descriptionBlob.GetDescriptionText())
newDescription := cleanupStringProperty(descriptionBlob.GetDescription())
decryptedGroupChange.ModifyDescription = &newDescription
}
}
@ -890,7 +928,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}
}
for _, addPendingMember := range encryptedActions.AddMembersPendingProfileKey {
for _, addPendingMember := range encryptedActions.AddPendingMembers {
if addPendingMember == nil {
continue
}
@ -903,7 +941,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember)
}
for _, deletePendingMember := range encryptedActions.DeleteMembersPendingProfileKey {
for _, deletePendingMember := range encryptedActions.DeletePendingMembers {
if deletePendingMember == nil {
continue
}
@ -916,7 +954,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID)
}
for _, promotePendingMember := range encryptedActions.PromoteMembersPendingProfileKey {
for _, promotePendingMember := range encryptedActions.PromotePendingMembers {
if promotePendingMember == nil {
continue
}
@ -935,7 +973,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}
}
for _, promotePendingPniAciMember := range encryptedActions.PromoteMembersPendingPniAciProfileKey {
for _, promotePendingPniAciMember := range encryptedActions.PromotePendingPniAciMembers {
// TODO: pretending this is a PendingMember should do for mautrix-signal, but we probably want to treat them separately at some point
if promotePendingPniAciMember == nil {
continue
@ -965,7 +1003,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}
}
for _, addRequestingMember := range encryptedActions.AddMembersPendingAdminApproval {
for _, addRequestingMember := range encryptedActions.AddRequestingMembers {
if addRequestingMember == nil {
continue
}
@ -981,7 +1019,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}
}
for _, deleteRequestingMember := range encryptedActions.DeleteMembersPendingAdminApproval {
for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers {
if deleteRequestingMember == nil {
continue
}
@ -994,7 +1032,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID)
}
for _, promoteRequestingMember := range encryptedActions.PromoteMembersPendingAdminApproval {
for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers {
if promoteRequestingMember == nil {
continue
}
@ -1010,7 +1048,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
})
}
for _, addBannedMember := range encryptedActions.AddMembersBanned {
for _, addBannedMember := range encryptedActions.AddBannedMembers {
if addBannedMember == nil {
continue
}
@ -1027,7 +1065,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
})
}
for _, deleteBannedMember := range encryptedActions.DeleteMembersBanned {
for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers {
if deleteBannedMember == nil {
continue
}
@ -1055,8 +1093,8 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
if encryptedActions.ModifyAnnouncementsOnly != nil {
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
}
if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer)
if encryptedActions.ModifyDisappearingMessagesTimer != nil && len(encryptedActions.ModifyDisappearingMessagesTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessagesTimer.Timer)
if err != nil {
return nil, err
}
@ -1068,12 +1106,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword
}
success = true
err = cli.GroupCache.ApplyUpdate(decryptedGroupChange, nil)
if err != nil {
log.Err(err).Msg("Failed to apply group change to cache")
}
return decryptedGroupChange, nil
}
@ -1127,11 +1159,11 @@ func decryptMember(ctx context.Context, member *signalpb.Member, groupSecretPara
ACI: *aci,
ProfileKey: *profileKey,
Role: GroupMemberRole(member.Role),
JoinedAtRevision: member.JoinedAtVersion,
JoinedAtRevision: member.JoinedAtRevision,
}, nil
}
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPendingProfileKey, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.PendingMember, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
log := zerolog.Ctx(ctx)
encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
@ -1154,7 +1186,7 @@ func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPen
}, nil
}
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.MemberPendingAdminApproval, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.RequestingMember, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams)
if err != nil {
return nil, err
@ -1175,7 +1207,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get groupSecretParams from master key")
return nil, err
}
groupChangeActions := &signalpb.GroupChange_Actions{Version: decryptedGroupChange.Revision}
groupChangeActions := &signalpb.GroupChange_Actions{Revision: decryptedGroupChange.Revision}
if decryptedGroupChange.ModifyTitle != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}}
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
@ -1186,7 +1218,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle}
}
if decryptedGroupChange.ModifyDescription != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: *decryptedGroupChange.ModifyDescription}}
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: *decryptedGroupChange.ModifyDescription}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt description")
@ -1208,7 +1240,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
JoinFromInviteLink: addMember.JoinFromInviteLink,
})
} else {
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember,
})
}
@ -1240,7 +1272,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err
}
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember,
})
}
@ -1250,7 +1282,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deletePendingMember")
return nil, err
}
groupChangeActions.DeleteMembersPendingProfileKey = append(groupChangeActions.DeleteMembersPendingProfileKey, &signalpb.GroupChange_Actions_DeleteMemberPendingProfileKeyAction{
groupChangeActions.DeletePendingMembers = append(groupChangeActions.DeletePendingMembers, &signalpb.GroupChange_Actions_DeletePendingMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
@ -1268,7 +1300,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err
}
groupChangeActions.PromoteMembersPendingProfileKey = append(groupChangeActions.PromoteMembersPendingProfileKey, &signalpb.GroupChange_Actions_PromoteMemberPendingProfileKeyAction{
groupChangeActions.PromotePendingMembers = append(groupChangeActions.PromotePendingMembers, &signalpb.GroupChange_Actions_PromotePendingMemberAction{
Presentation: *presentation,
})
}
@ -1286,8 +1318,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err
}
groupChangeActions.AddMembersPendingAdminApproval = append(groupChangeActions.AddMembersPendingAdminApproval, &signalpb.GroupChange_Actions_AddMemberPendingAdminApprovalAction{
Added: &signalpb.MemberPendingAdminApproval{
groupChangeActions.AddRequestingMembers = append(groupChangeActions.AddRequestingMembers, &signalpb.GroupChange_Actions_AddRequestingMemberAction{
Added: &signalpb.RequestingMember{
Presentation: *presentation,
},
})
@ -1298,7 +1330,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember")
return nil, err
}
groupChangeActions.DeleteMembersPendingAdminApproval = append(groupChangeActions.DeleteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_DeleteMemberPendingAdminApprovalAction{
groupChangeActions.DeleteRequestingMembers = append(groupChangeActions.DeleteRequestingMembers, &signalpb.GroupChange_Actions_DeleteRequestingMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
@ -1309,7 +1341,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return nil, err
}
groupChangeActions.PromoteMembersPendingAdminApproval = append(groupChangeActions.PromoteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_PromoteMemberPendingAdminApprovalAction{
groupChangeActions.PromoteRequestingMembers = append(groupChangeActions.PromoteRequestingMembers, &signalpb.GroupChange_Actions_PromoteRequestingMemberAction{
UserId: encryptedUserID[:],
Role: signalpb.Member_Role(promoteRequestingMember.Role),
})
@ -1320,8 +1352,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err
}
groupChangeActions.AddMembersBanned = append(groupChangeActions.AddMembersBanned, &signalpb.GroupChange_Actions_AddMemberBannedAction{
Added: &signalpb.MemberBanned{
groupChangeActions.AddBannedMembers = append(groupChangeActions.AddBannedMembers, &signalpb.GroupChange_Actions_AddBannedMemberAction{
Added: &signalpb.BannedMember{
UserId: encryptedUserID[:],
Timestamp: addBannedMember.Timestamp,
},
@ -1333,7 +1365,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err
}
groupChangeActions.DeleteMembersBanned = append(groupChangeActions.DeleteMembersBanned, &signalpb.GroupChange_Actions_DeleteMemberBannedAction{
groupChangeActions.DeleteBannedMembers = append(groupChangeActions.DeleteBannedMembers, &signalpb.GroupChange_Actions_DeleteBannedMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
@ -1364,7 +1396,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get encrypt Title")
return nil, err
}
groupChangeActions.ModifyDisappearingMessageTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessageTimerAction{Timer: *encryptedTimer}
groupChangeActions.ModifyDisappearingMessagesTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessagesTimerAction{Timer: *encryptedTimer}
}
if decryptedGroupChange.ModifyInviteLinkPassword != nil {
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword)
@ -1379,7 +1411,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil)
}
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.MemberPendingProfileKey, error) {
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx)
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI)
if err != nil {
@ -1407,7 +1439,7 @@ func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, group
return &encryptedMember, nil, nil
}
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.MemberPendingProfileKey, error) {
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx)
encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID)
if err != nil {
@ -1419,7 +1451,7 @@ func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *Pend
log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember")
return nil, err
}
encryptedPendingMember := signalpb.MemberPendingProfileKey{
encryptedPendingMember := signalpb.PendingMember{
AddedByUserId: encryptedAddedByUserID[:],
Member: &signalpb.Member{
UserId: encryptedUserID[:],
@ -1472,9 +1504,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Body: requestBody,
Host: web.StorageHostname,
}
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPatch, path, opts)
defer web.CloseBody(resp)
resp, err := web.SendHTTPRequest(ctx, http.MethodPatch, path, opts)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
@ -1513,21 +1545,17 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
return &changeResp, nil
}
var ErrGroupMasterKeyNotFound = errors.New("group master key not found in store")
func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
return 0, fmt.Errorf("failed to get master key for group: %w", err)
} else if groupMasterKey == "" {
return 0, ErrGroupMasterKeyNotFound
}
groupChange.GroupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChangeResponse
group, _, err := cli.RetrieveGroupByID(ctx, gid, 0)
group, err := cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil {
return 0, fmt.Errorf("failed to fetch group info to update: %w", err)
}
@ -1550,8 +1578,10 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("failed to update group: %w", err)
}
} else if errors.Is(err, ConflictError) {
cli.GroupCache.Delete(gid)
group, _, err = cli.RetrieveGroupByID(ctx, gid, 0)
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
group, err = cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil {
return 0, fmt.Errorf("failed to fetch group after conflict: %w", err)
}
@ -1564,13 +1594,12 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err)
}
}
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
if signedGroupChange == nil {
return 0, fmt.Errorf("no signed group change returned: %w", err)
}
err = cli.GroupCache.ApplyUpdate(groupChange, signedGroupChange.GroupSendEndorsementsResponse)
if err != nil {
log.Err(err).Msg("Failed to apply group change to cache")
}
groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
if err != nil {
return 0, fmt.Errorf("failed to marshal signed group change: %w", err)
@ -1603,12 +1632,12 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
encryptedGroup := &signalpb.Group{
PublicKey: groupPublicParams[:],
Title: *encryptedTitle,
AvatarUrl: decryptedGroup.AvatarPath,
Avatar: decryptedGroup.AvatarPath,
AnnouncementsOnly: decryptedGroup.AnnouncementsOnly,
Version: 0,
Revision: 0,
}
if decryptedGroup.Description != "" {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: decryptedGroup.Description}}
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: decryptedGroup.Description}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt Description")
@ -1634,7 +1663,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
if encryptedMember != nil {
encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember)
} else {
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember)
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
}
}
for _, pendingMember := range decryptedGroup.PendingMembers {
@ -1643,7 +1672,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err
}
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember)
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
}
return encryptedGroup, nil
}
@ -1666,7 +1695,7 @@ func PrepareGroupCreation(decryptedGroup *Group) (libsignalgo.GroupMasterKey, er
return masterKeyBytes, nil
}
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group) (*Group, error) {
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger()
masterKeyBytes, err := PrepareGroupCreation(decryptedGroup)
if err != nil {
@ -1681,6 +1710,14 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err
}
if len(avatarBytes) > 0 {
avatarPath, err := cli.UploadGroupAvatar(ctx, avatarBytes, decryptedGroup.GroupIdentifier)
if err != nil {
log.Err(err).Msg("Failed to upload group avatar")
return nil, err
}
decryptedGroup.AvatarPath = avatarPath
}
encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt group")
@ -1702,9 +1739,9 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Body: requestBody,
Host: web.StorageHostname,
}
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPut, path, opts)
defer web.CloseBody(resp)
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, path, opts)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
@ -1731,9 +1768,9 @@ func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
return InviteLinkPasswordFromBytes(random.Bytes(16))
}
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group) (*Group, error) {
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroup").Logger()
group, err := cli.createGroupOnServer(ctx, decryptedGroup)
group, err := cli.createGroupOnServer(ctx, decryptedGroup, avatarBytes)
if err != nil {
log.Err(err).Msg("Error creating group on server")
return nil, err
@ -1756,7 +1793,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
return nil, err
}
if groupMasterKey == "" {
return nil, ErrGroupMasterKeyNotFound
return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
}
masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
@ -1767,6 +1804,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
Username: &groupAuth.Username,
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Host: web.StorageHostname,
Headers: map[string]string{
// TODO actually cache the data and provide real expiry timestamp
"Cached-Send-Endorsements": "0",
@ -1774,13 +1812,12 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
}
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false
path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState)
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts)
defer web.CloseBody(response)
response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, fmt.Errorf("unexpected response status: %d", response.StatusCode)
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
}
var encryptedGroupChanges signalpb.GroupChanges
groupChangesBytes, err := io.ReadAll(response.Body)

View file

@ -22,7 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand/v2"
"math/rand"
"net/http"
"strings"
"time"
@ -43,6 +43,25 @@ type GeneratedPreKeys struct {
IdentityKey []uint8
}
func (cli *Client) GenerateAndRegisterPreKeys(ctx context.Context, pks store.PreKeyStore) error {
_, err := cli.GenerateAndSaveNextPreKeyBatch(ctx, pks, 0)
if err != nil {
return fmt.Errorf("failed to generate and save next prekey batch: %w", err)
}
_, err = cli.GenerateAndSaveNextKyberPreKeyBatch(ctx, pks, 0)
if err != nil {
return fmt.Errorf("failed to generate and save next kyber prekey batch: %w", err)
}
// We need to upload all currently valid prekeys, not just the ones we just generated
err = cli.RegisterAllPreKeys(ctx, pks)
if err != nil {
return fmt.Errorf("failed to register prekey batches: %w", err)
}
return err
}
func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore) error {
var identityKeyPair *libsignalgo.IdentityKeyPair
var pni bool
@ -78,11 +97,10 @@ func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore
KyberPreKeys: kyberPreKeys,
IdentityKey: identityKey,
}
zerolog.Ctx(ctx).Debug().
Int("num_prekeys", len(preKeys)).
Int("num_kyber_prekeys", len(kyberPreKeys)).
Msg("Registering all prekeys")
err = cli.RegisterPreKeys(ctx, &generatedPreKeys, pni)
preKeyUsername := fmt.Sprintf("%s.%d", cli.Store.ACI, cli.Store.DeviceID)
log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
log.Debug().Int("num_prekeys", len(preKeys)).Int("num_kyber_prekeys", len(kyberPreKeys)).Msg("Registering prekeys")
err = RegisterPreKeys(ctx, &generatedPreKeys, pni, preKeyUsername, cli.Store.Password)
if err != nil {
return fmt.Errorf("failed to register prekeys: %w", err)
}
@ -328,11 +346,11 @@ func KyberPreKeyToJSON(kyberPreKey *libsignalgo.KyberPreKeyRecord) (map[string]i
var errPrekeyUpload422 = errors.New("http 422 while registering prekeys")
func (cli *Client) RegisterPreKeys(ctx context.Context, generatedPreKeys *GeneratedPreKeys, pni bool) error {
func RegisterPreKeys(ctx context.Context, generatedPreKeys *GeneratedPreKeys, pni bool, username string, password string) error {
log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
// Convert generated prekeys to JSON
preKeysJson := []map[string]any{}
kyberPreKeysJson := []map[string]any{}
preKeysJson := []map[string]interface{}{}
kyberPreKeysJson := []map[string]interface{}{}
for _, preKey := range generatedPreKeys.PreKeys {
preKeyJson, err := PreKeyToJSON(preKey)
if err != nil {
@ -349,27 +367,32 @@ func (cli *Client) RegisterPreKeys(ctx context.Context, generatedPreKeys *Genera
}
identityKey := generatedPreKeys.IdentityKey
registerJSON := map[string]any{
register_json := map[string]interface{}{
"preKeys": preKeysJson,
"pqPreKeys": kyberPreKeysJson,
"identityKey": base64.StdEncoding.EncodeToString(identityKey),
}
// Send request
jsonBytes, err := json.Marshal(registerJSON)
jsonBytes, err := json.Marshal(register_json)
if err != nil {
log.Err(err).Msg("Error marshalling register JSON")
return err
}
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, keysPath(pni), jsonBytes, nil)
opts := &web.HTTPReqOpt{Body: jsonBytes, Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, keysPath(pni), opts)
if err != nil {
log.Err(err).Msg("Error sending request")
return err
}
if resp.GetStatus() == 422 {
defer resp.Body.Close()
// status code not 2xx
if resp.StatusCode == 422 {
return errPrekeyUpload422
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("error registering prekeys: %v", resp.Status)
}
return web.DecodeWSResponseBody(ctx, nil, resp)
return err
}
type prekeyResponse struct {
@ -404,40 +427,25 @@ func addBase64PaddingAndDecode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data)
}
var (
ErrUnregisteredUser = errors.New("user is unregistered")
ErrDevicesChanged = errors.New("device list changed while sending skdm")
)
func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID libsignalgo.ServiceID, specificDeviceID int) error {
if cli.Store.RecipientStore.IsUnregistered(ctx, theirServiceID) {
return fmt.Errorf("%w (cached)", ErrUnregisteredUser)
}
localAddress, err := cli.Store.ACIServiceID().Address(uint(cli.Store.DeviceID))
if err != nil {
return fmt.Errorf("failed to get own address: %w", err)
}
// Fetch prekey
deviceIDPath := "/*"
if specificDeviceID >= 0 {
deviceIDPath = "/" + fmt.Sprint(specificDeviceID)
}
// TODO this should be done via the unauthed websocket if possible
path := "/v2/keys/" + theirServiceID.String() + deviceIDPath + "?pq=true"
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, path, nil, nil)
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, path, &web.HTTPReqOpt{Username: &username, Password: &password})
if err != nil {
return fmt.Errorf("error sending request: %w", err)
} else if resp.GetStatus() == 404 {
cli.Store.RecipientStore.MarkUnregistered(ctx, theirServiceID, true)
return fmt.Errorf("%w (404 while querying keys)", ErrUnregisteredUser)
}
var respData prekeyResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp)
var prekeyResponse prekeyResponse
err = web.DecodeHTTPResponseBody(ctx, &prekeyResponse, resp)
if err != nil {
return fmt.Errorf("error decoding response body: %w", err)
}
rawIdentityKey, err := addBase64PaddingAndDecode(respData.IdentityKey)
rawIdentityKey, err := addBase64PaddingAndDecode(prekeyResponse.IdentityKey)
if err != nil {
return fmt.Errorf("error decoding identity key: %w", err)
}
@ -450,7 +458,7 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
}
// Process each prekey in response (should only be one at the moment)
for _, d := range respData.Devices {
for _, d := range prekeyResponse.Devices {
var publicKey *libsignalgo.PublicKey
var preKeyID uint32
if d.PreKey != nil {
@ -522,7 +530,6 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
ctx,
preKeyBundle,
address,
localAddress,
cli.Store.ACISessionStore,
cli.Store.ACIIdentityStore,
)
@ -548,18 +555,19 @@ func keysPath(pni bool) string {
func (cli *Client) GetMyKeyCounts(ctx context.Context, pni bool) (int, int, error) {
log := zerolog.Ctx(ctx).With().Str("action", "get my key counts").Logger()
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, keysPath(pni), nil, nil)
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, keysPath(pni), &web.HTTPReqOpt{Username: &username, Password: &password})
if err != nil {
log.Err(err).Msg("Error sending request")
return 0, 0, err
}
var respData preKeyCountResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp)
var preKeyCountResponse preKeyCountResponse
err = web.DecodeHTTPResponseBody(ctx, &preKeyCountResponse, resp)
if err != nil {
log.Err(err).Msg("Fetching prekey counts, error with response body")
return 0, 0, err
}
return respData.Count, respData.PQCount, err
return preKeyCountResponse.Count, preKeyCountResponse.PQCount, err
}
func (cli *Client) CheckAndUploadNewPreKeys(ctx context.Context, pks store.PreKeyStore) error {
@ -596,29 +604,23 @@ func (cli *Client) keyCheckLoop(ctx context.Context) {
log := zerolog.Ctx(ctx).With().Str("action", "start key check loop").Logger()
// Do the initial check in 5-10 minutes after starting the loop
windowStart := 0
windowSize := 1
firstRun := true
window_start := 0
window_size := 1
for {
randomMinutesInWindow := rand.IntN(windowSize) + windowStart
checkTime := time.Duration(randomMinutesInWindow) * time.Minute
if firstRun {
checkTime = 0
firstRun = false
} else {
log.Debug().Dur("check_time", checkTime).Msg("Waiting to check for new prekeys")
}
random_minutes_in_window := rand.Intn(window_size) + window_start
check_time := time.Duration(random_minutes_in_window) * time.Minute
log.Debug().Dur("check_time", check_time).Msg("Waiting to check for new prekeys")
select {
case <-ctx.Done():
return
case <-time.After(checkTime):
case <-time.After(check_time):
err := cli.CheckAndUploadNewPreKeys(ctx, cli.Store.ACIPreKeyStore)
if err != nil {
log.Err(err).Msg("Error checking and uploading new prekeys for ACI identity")
// Retry within half an hour
windowStart = 5
windowSize = 25
window_start = 5
window_size = 25
continue
}
err = cli.CheckAndUploadNewPreKeys(ctx, cli.Store.PNIPreKeyStore)
@ -634,13 +636,13 @@ func (cli *Client) keyCheckLoop(ctx context.Context) {
}
log.Err(err).Msg("Error checking and uploading new prekeys for PNI identity")
// Retry within half an hour
windowStart = 5
windowSize = 25
window_start = 5
window_size = 25
continue
}
// After a successful check, check again in 36 to 60 hours
windowStart = 36 * 60
windowSize = 24 * 60
window_start = 36 * 60
window_size = 24 * 60
}
}
}

View file

@ -18,10 +18,7 @@ package signalmeow
import (
_ "embed"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
@ -69,8 +66,6 @@ func (l FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, messa
func (FFILogger) Flush() {}
func (FFILogger) Destroy() {}
// Ensure FFILogger implements the Logger interface
var _ libsignalgo.Logger = FFILogger{}
@ -81,28 +76,3 @@ var prodServerPublicParams *libsignalgo.ServerPublicParams
func init() {
prodServerPublicParams = exerrors.Must(libsignalgo.DeserializeServerPublicParams(prodServerPublicParamsSlice))
}
var ErrEmptyUUIDInput = errors.New("both input variables are empty")
func ParseStringOrBinaryServiceID(str string, bytes []byte) (libsignalgo.ServiceID, error) {
if str != "" {
return libsignalgo.ServiceIDFromString(str)
}
if bytes != nil {
return libsignalgo.ServiceIDFromBytes(bytes)
}
return libsignalgo.EmptyServiceID, ErrEmptyUUIDInput
}
func ParseStringOrBinaryUUID(str string, bytes []byte) (uuid.UUID, error) {
if str != "" {
return uuid.Parse(str)
}
if bytes != nil {
if len(bytes) != 16 {
return uuid.Nil, fmt.Errorf("invalid UUID length %d (expected 16)", len(bytes))
}
return uuid.UUID(bytes), nil
}
return uuid.Nil, ErrEmptyUUIDInput
}

View file

@ -134,6 +134,14 @@ func (cli *Client) getCachedProfileByID(signalID uuid.UUID, refreshAfter time.Du
}
func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID, refreshAfter time.Duration) (*types.Profile, error) {
if cli.ProfileCache == nil {
cli.ProfileCache = &ProfileCache{
profiles: make(map[string]*types.Profile),
errors: make(map[string]*error),
lastFetched: make(map[string]time.Time),
}
}
// Check if we have a cached profile that is less than an hour old
// or if we have a cached error that is less than an hour old
profile, err := cli.getCachedProfileByID(signalID, refreshAfter)
@ -210,18 +218,18 @@ func (cli *Client) fetchProfileWithRequestAndKey(ctx context.Context, signalID u
path += "/" + string(credentialRequest)
path += "?credentialType=expiringProfileKey"
}
headers := http.Header{}
profileRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
if useUnidentified {
headers.Set("Unidentified-Access-Key", base64AccessKey)
headers.Set("Accept-Language", "en-US")
profileRequest.Headers = append(profileRequest.Headers, "unidentified-access-key:"+base64AccessKey)
profileRequest.Headers = append(profileRequest.Headers, "accept-language:en-CA")
}
resp, err := cli.UnauthedWS.SendRequest(ctx, http.MethodGet, path, nil, headers)
resp, err := cli.UnauthedWS.SendRequest(ctx, profileRequest)
if err != nil {
return nil, fmt.Errorf("error sending request: %w", err)
}
var profile types.Profile
profile.FetchedAt = time.Now()
logEvt := log.Trace().Uint32("status_code", resp.GetStatus()).Str("resp_message", resp.GetMessage())
logEvt := log.Trace().Uint32("status_code", resp.GetStatus())
if logEvt.Enabled() {
if json.Valid(resp.Body) {
logEvt.RawJSON("response_data", resp.Body)
@ -280,14 +288,15 @@ func (cli *Client) fetchProfileWithRequestAndKey(ctx context.Context, signalID u
func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey libsignalgo.ProfileKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username,
Password: &password,
}
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts)
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer web.CloseBody(resp)
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode)
}

View file

@ -0,0 +1,272 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: ContactDiscovery.proto
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
package signalpb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type CDSClientRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
// by its 16-byte UAK.
AciUakPairs []byte `protobuf:"bytes,1,opt,name=aci_uak_pairs,json=aciUakPairs" json:"aci_uak_pairs,omitempty"`
// Each E164 is an 8-byte big-endian number, as 8 bytes.
PrevE164S []byte `protobuf:"bytes,2,opt,name=prev_e164s,json=prevE164s" json:"prev_e164s,omitempty"`
NewE164S []byte `protobuf:"bytes,3,opt,name=new_e164s,json=newE164s" json:"new_e164s,omitempty"`
DiscardE164S []byte `protobuf:"bytes,4,opt,name=discard_e164s,json=discardE164s" json:"discard_e164s,omitempty"`
// If true, the client has more pairs or e164s to send. If false or unset,
// this is the client's last request, and processing should commence.
HasMore *bool `protobuf:"varint,5,opt,name=has_more,json=hasMore" json:"has_more,omitempty"`
// If set, a token which allows rate limiting to discount the e164s in
// the request's prev_e164s, only counting new_e164s. If not set, then
// rate limiting considers both prev_e164s' and new_e164s' size.
Token []byte `protobuf:"bytes,6,opt,name=token" json:"token,omitempty"`
// After receiving a new token from the server, send back a message just
// containing a token_ack.
TokenAck *bool `protobuf:"varint,7,opt,name=token_ack,json=tokenAck" json:"token_ack,omitempty"`
// Request that, if the server allows, both ACI and PNI be returned even
// if the aci_uak_pairs don't match.
ReturnAcisWithoutUaks *bool `protobuf:"varint,8,opt,name=return_acis_without_uaks,json=returnAcisWithoutUaks" json:"return_acis_without_uaks,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CDSClientRequest) Reset() {
*x = CDSClientRequest{}
mi := &file_ContactDiscovery_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CDSClientRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CDSClientRequest) ProtoMessage() {}
func (x *CDSClientRequest) ProtoReflect() protoreflect.Message {
mi := &file_ContactDiscovery_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CDSClientRequest.ProtoReflect.Descriptor instead.
func (*CDSClientRequest) Descriptor() ([]byte, []int) {
return file_ContactDiscovery_proto_rawDescGZIP(), []int{0}
}
func (x *CDSClientRequest) GetAciUakPairs() []byte {
if x != nil {
return x.AciUakPairs
}
return nil
}
func (x *CDSClientRequest) GetPrevE164S() []byte {
if x != nil {
return x.PrevE164S
}
return nil
}
func (x *CDSClientRequest) GetNewE164S() []byte {
if x != nil {
return x.NewE164S
}
return nil
}
func (x *CDSClientRequest) GetDiscardE164S() []byte {
if x != nil {
return x.DiscardE164S
}
return nil
}
func (x *CDSClientRequest) GetHasMore() bool {
if x != nil && x.HasMore != nil {
return *x.HasMore
}
return false
}
func (x *CDSClientRequest) GetToken() []byte {
if x != nil {
return x.Token
}
return nil
}
func (x *CDSClientRequest) GetTokenAck() bool {
if x != nil && x.TokenAck != nil {
return *x.TokenAck
}
return false
}
func (x *CDSClientRequest) GetReturnAcisWithoutUaks() bool {
if x != nil && x.ReturnAcisWithoutUaks != nil {
return *x.ReturnAcisWithoutUaks
}
return false
}
type CDSClientResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
// If the e164 was not found, PNI and ACI are all zeros. If the PNI
// was found but the ACI was not, the PNI will be non-zero and the ACI
// will be all zeros. ACI will be returned if one of the returned
// PNIs has an ACI/UAK pair that matches.
//
// Should the request be successful (IE: a successful status returned),
// |e164_pni_aci_triple| will always equal |e164| of the request,
// so the entire marshalled size of the response will be (2+32)*|e164|,
// where the additional 2 bytes are the id/type/length additions of the
// protobuf marshaling added to each byte array. This avoids any data
// leakage based on the size of the encrypted output.
E164PniAciTriples []byte `protobuf:"bytes,1,opt,name=e164_pni_aci_triples,json=e164PniAciTriples" json:"e164_pni_aci_triples,omitempty"`
// A token which allows subsequent calls' rate limiting to discount the
// e164s sent up in this request, only counting those in the next
// request's new_e164s.
Token []byte `protobuf:"bytes,3,opt,name=token" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CDSClientResponse) Reset() {
*x = CDSClientResponse{}
mi := &file_ContactDiscovery_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CDSClientResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CDSClientResponse) ProtoMessage() {}
func (x *CDSClientResponse) ProtoReflect() protoreflect.Message {
mi := &file_ContactDiscovery_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CDSClientResponse.ProtoReflect.Descriptor instead.
func (*CDSClientResponse) Descriptor() ([]byte, []int) {
return file_ContactDiscovery_proto_rawDescGZIP(), []int{1}
}
func (x *CDSClientResponse) GetE164PniAciTriples() []byte {
if x != nil {
return x.E164PniAciTriples
}
return nil
}
func (x *CDSClientResponse) GetToken() []byte {
if x != nil {
return x.Token
}
return nil
}
var File_ContactDiscovery_proto protoreflect.FileDescriptor
const file_ContactDiscovery_proto_rawDesc = "" +
"\n" +
"\x16ContactDiscovery.proto\x12\rsignalservice\"\x9e\x02\n" +
"\x10CDSClientRequest\x12\"\n" +
"\raci_uak_pairs\x18\x01 \x01(\fR\vaciUakPairs\x12\x1d\n" +
"\n" +
"prev_e164s\x18\x02 \x01(\fR\tprevE164s\x12\x1b\n" +
"\tnew_e164s\x18\x03 \x01(\fR\bnewE164s\x12#\n" +
"\rdiscard_e164s\x18\x04 \x01(\fR\fdiscardE164s\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore\x12\x14\n" +
"\x05token\x18\x06 \x01(\fR\x05token\x12\x1b\n" +
"\ttoken_ack\x18\a \x01(\bR\btokenAck\x127\n" +
"\x18return_acis_without_uaks\x18\b \x01(\bR\x15returnAcisWithoutUaks\"Z\n" +
"\x11CDSClientResponse\x12/\n" +
"\x14e164_pni_aci_triples\x18\x01 \x01(\fR\x11e164PniAciTriples\x12\x14\n" +
"\x05token\x18\x03 \x01(\fR\x05token"
var (
file_ContactDiscovery_proto_rawDescOnce sync.Once
file_ContactDiscovery_proto_rawDescData []byte
)
func file_ContactDiscovery_proto_rawDescGZIP() []byte {
file_ContactDiscovery_proto_rawDescOnce.Do(func() {
file_ContactDiscovery_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ContactDiscovery_proto_rawDesc), len(file_ContactDiscovery_proto_rawDesc)))
})
return file_ContactDiscovery_proto_rawDescData
}
var file_ContactDiscovery_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_ContactDiscovery_proto_goTypes = []any{
(*CDSClientRequest)(nil), // 0: signalservice.CDSClientRequest
(*CDSClientResponse)(nil), // 1: signalservice.CDSClientResponse
}
var file_ContactDiscovery_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_ContactDiscovery_proto_init() }
func file_ContactDiscovery_proto_init() {
if File_ContactDiscovery_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ContactDiscovery_proto_rawDesc), len(file_ContactDiscovery_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_ContactDiscovery_proto_goTypes,
DependencyIndexes: file_ContactDiscovery_proto_depIdxs,
MessageInfos: file_ContactDiscovery_proto_msgTypes,
}.Build()
File_ContactDiscovery_proto = out.File
file_ContactDiscovery_proto_goTypes = nil
file_ContactDiscovery_proto_depIdxs = nil
}

View file

@ -1,36 +1,37 @@
syntax = "proto3";
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
package org.signal.cdsi;
package signalservice;
message ClientRequest {
message CDSClientRequest {
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
// by its 16-byte UAK.
bytes aci_uak_pairs = 1;
optional bytes aci_uak_pairs = 1;
// Each E164 is an 8-byte big-endian number, as 8 bytes.
bytes prev_e164s = 2;
bytes new_e164s = 3;
bytes discard_e164s = 4;
optional bytes prev_e164s = 2;
optional bytes new_e164s = 3;
optional bytes discard_e164s = 4;
// If true, the client has more pairs or e164s to send. If false or unset,
// this is the client's last request, and processing should commence.
// bool has_more = 5;
reserved 5;
optional bool has_more = 5;
// If set, a token which allows rate limiting to discount the e164s in
// the request's prev_e164s, only counting new_e164s. If not set, then
// rate limiting considers both prev_e164s' and new_e164s' size.
bytes token = 6;
optional bytes token = 6;
// After receiving a new token from the server, send back a message just
// containing a token_ack.
bool token_ack = 7;
optional bool token_ack = 7;
// [deprecated] bool return_acis_without_uaks = 8
reserved 8;
// Request that, if the server allows, both ACI and PNI be returned even
// if the aci_uak_pairs don't match.
optional bool return_acis_without_uaks = 8;
}
message ClientResponse {
message CDSClientResponse {
// Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
// If the e164 was not found, PNI and ACI are all zeros. If the PNI
// was found but the ACI was not, the PNI will be non-zero and the ACI
@ -43,18 +44,10 @@ message ClientResponse {
// where the additional 2 bytes are the id/type/length additions of the
// protobuf marshaling added to each byte array. This avoids any data
// leakage based on the size of the encrypted output.
bytes e164_pni_aci_triples = 1;
// int32 retry_after_secs = 2 [deprecated]
reserved 2;
optional bytes e164_pni_aci_triples = 1;
// A token which allows subsequent calls' rate limiting to discount the
// e164s sent up in this request, only counting those in the next
// request's new_e164s.
bytes token = 3;
// On a successful response to a token_ack request, the number of permits
// that were deducted from the user's rate-limit in order to process the
// request
int32 debug_permits_used = 4;
optional bytes token = 3;
}

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.1
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: DeviceName.proto
// Copyright 2018 Signal Messenger, LLC

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more