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

Compare commits

..
Author SHA1 Message Date
Tulir Asokan
7ffa38469f groupinfo: add exclude from timeline flag for group resyncs 2025-10-01 15:30:21 +03:00
Tulir Asokan
606d904cac dependencies: update mautrix-go 2025-10-01 15:30:18 +03:00
Tulir Asokan
c795cea7a1 signalmeow/groups: update to v2 api 2025-10-01 15:30:15 +03:00
148 changed files with 7744 additions and 18329 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 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
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. --> ignored or closed immediately.
-->
* [ ] 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: ``

View file

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

View file

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

View file

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

View file

@ -1,100 +1,3 @@
# v26.08
* 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
* Switched to calendar versioning.
* Updated libsignal to v0.84.0.
* Fixed backfill creating incorrect disappearing timer change notices.
# v0.8.7 (2025-09-16) # v0.8.7 (2025-09-16)
* Removed legacy provisioning API and database legacy migration. * Removed legacy provisioning API and database legacy migration.

View file

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

View file

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

View file

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

View file

@ -1,2 +1,9 @@
#!/bin/sh #!/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 #!/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 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

@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
set -e
./build-rust.sh ./build-rust.sh
cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a . cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh

View file

@ -17,12 +17,9 @@
package main package main
import ( import (
"fmt"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain" "maunium.net/go/mautrix/bridgev2/matrix/mxmain"
"go.mau.fi/mautrix-signal/pkg/connector" "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. // Information to find out exactly which commit the bridge was built from.
@ -37,14 +34,12 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal", Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal", URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.", Description: "A Matrix-Signal puppeting bridge.",
Version: "26.08", Version: "0.8.7",
SemCalVer: true,
Connector: &connector.SignalConnector{}, Connector: &connector.SignalConnector{},
} }
func main() { func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.PostStart = func() { m.PostStart = func() {
if m.Matrix.Provisioning != nil { if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier) 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 module go.mau.fi/mautrix-signal
go 1.25.0 go 1.24.0
toolchain go1.26.6 toolchain go1.25.1
tool go.mau.fi/util/cmd/maubuild
require ( 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/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mattn/go-pointer v0.0.1 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/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.10.0 go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10
golang.org/x/crypto v0.55.0 golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 golang.org/x/exp v0.0.0-20250911091902-df9299821621
golang.org/x/net v0.58.0 golang.org/x/net v0.44.0
golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.9
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.30.0 maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf
) )
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.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-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // 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/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // 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 go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.29.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

88
go.sum
View file

@ -1,18 +1,19 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= 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 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.14/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.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= 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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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/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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 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.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= 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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= 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.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.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 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.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10 h1:EvX/di02gOriKN0xGDJuQ5mgiNdAF4LJc8moffI7Svo=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= 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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= 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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= 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.25.2-0.20251001115535-dd778ae0cdaf h1:prmIYgiziW4A8H2v/TliQ7fis8uTWblabxyPIeLFlNg=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo= maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf/go.mod h1:eWXuX2UAGye4AU7i/8Fv2L2Nh7L9kZtuv3R0O0n1KaM=

View file

@ -151,7 +151,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
if dm == nil { if dm == nil {
continue 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)) convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions))
for _, reaction := range reactions { for _, reaction := range reactions {
reactionSenderACI, err := getRecipientACI(reaction.AuthorId) reactionSenderACI, err := getRecipientACI(reaction.AuthorId)
@ -187,7 +187,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
CompleteCallback: func() { CompleteCallback: func() {
// When reaching the last backwards backfill batch, delete the chat from the backup store. // 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 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) err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store") 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" "go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
) )
@ -38,7 +37,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.signal.capabilities.2026_07_22" base := "fi.mau.signal.capabilities.2025_08_25"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -77,7 +76,6 @@ var signalCaps = &event.RoomFeatures{
"image/jpeg": event.CapLevelFullySupported, "image/jpeg": event.CapLevelFullySupported,
"image/webp": event.CapLevelFullySupported, "image/webp": event.CapLevelFullySupported,
"image/bmp": event.CapLevelFullySupported, "image/bmp": event.CapLevelFullySupported,
"image/avif": event.CapLevelFullySupported,
}, },
MaxWidth: 4096, MaxWidth: 4096,
MaxHeight: 4096, MaxHeight: 4096,
@ -99,8 +97,6 @@ var signalCaps = &event.RoomFeatures{
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/aac": event.CapLevelFullySupported, "audio/aac": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported, "audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported,
}, },
MaxSize: MaxFileSize, MaxSize: MaxFileSize,
}, },
@ -114,8 +110,7 @@ var signalCaps = &event.RoomFeatures{
}, },
event.CapMsgSticker: { event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
// Signal clients will only render static webp, so apng is preferred "image/webp": event.CapLevelFullySupported,
"image/webp": event.CapLevelPartialSupport,
"image/png": event.CapLevelFullySupported, "image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported, "image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(), "image/gif": supportedIfFFmpeg(),
@ -141,36 +136,18 @@ var signalCaps = &event.RoomFeatures{
MaxSize: MaxFileSize, MaxSize: MaxFileSize,
}, },
}, },
State: event.StateFeatureMap{ MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
event.StateRoomName.Type: {Level: event.CapLevelFullySupported}, LocationMessage: event.CapLevelPartialSupport,
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported}, Poll: event.CapLevelRejected,
event.StateTopic.Type: {Level: event.CapLevelFullySupported}, Thread: event.CapLevelUnsupported,
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported}, Reply: event.CapLevelFullySupported,
}, Edit: event.CapLevelFullySupported,
MemberActions: event.MemberFeatureMap{ EditMaxCount: 10,
event.MemberActionInvite: event.CapLevelFullySupported, EditMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
event.MemberActionRevokeInvite: event.CapLevelFullySupported, Delete: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported, DeleteForMe: false,
event.MemberActionBan: event.CapLevelFullySupported, DeleteMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
event.MemberActionKick: event.CapLevelFullySupported, DisappearingTimer: signalDisappearingCap,
},
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,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
EditMaxCount: 10,
EditMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
Delete: event.CapLevelFullySupported,
DeleteForMe: false,
DeleteMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
DisappearingTimer: signalDisappearingCap,
Reaction: event.CapLevelFullySupported, Reaction: event.CapLevelFullySupported,
ReactionCount: 1, ReactionCount: 1,
@ -178,12 +155,6 @@ var signalCaps = &event.RoomFeatures{
CustomEmojiReactions: false, CustomEmojiReactions: false,
ReadReceipts: true, ReadReceipts: true,
TypingNotifications: true, TypingNotifications: true,
DeleteChat: true,
MessageRequest: &event.MessageRequestFeatures{
AcceptWithMessage: event.CapLevelPartialSupport,
AcceptWithButton: event.CapLevelFullySupported,
},
} }
var signalDisappearingCap = &event.DisappearingTimerCapability{ var signalDisappearingCap = &event.DisappearingTimerCapability{
@ -191,16 +162,9 @@ var signalDisappearingCap = &event.DisappearingTimerCapability{
} }
var signalCapsNoteToSelf *event.RoomFeatures var signalCapsNoteToSelf *event.RoomFeatures
var signalCapsDM *event.RoomFeatures
func init() { func init() {
signalCapsDM = ptr.Clone(signalCaps) signalCapsNoteToSelf = 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.EditMaxAge = nil signalCapsNoteToSelf.EditMaxAge = nil
signalCapsNoteToSelf.DeleteMaxAge = nil signalCapsNoteToSelf.DeleteMaxAge = nil
signalCapsNoteToSelf.ID = capID() + "+note_to_self" signalCapsNoteToSelf.ID = capID() + "+note_to_self"
@ -209,8 +173,6 @@ func init() {
func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures { 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) { if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
return signalCapsNoteToSelf return signalCapsNoteToSelf
} else if portal.RoomType == database.RoomTypeDM {
return signalCapsDM
} }
return signalCaps return signalCaps
} }
@ -220,7 +182,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true, AggressiveUpdateInfo: true,
ImplicitReadReceipts: true, ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{ Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true, CreateDM: true,
LookupPhone: true, LookupPhone: true,
@ -245,5 +206,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
} }
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 11 return 1, 5
} }

View file

@ -45,16 +45,13 @@ var (
_ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil) _ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil) _ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.GhostDMCreatingNetworkAPI = (*SignalClient)(nil)
) )
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
const PrivateChatTopic = "Signal private chat" const PrivateChatTopic = "Signal private chat"
const NoteToSelfName = "Signal Note to Self" const NoteToSelfName = "Signal Note to Self"
func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *bridgev2.Ghost, refreshAfter time.Duration) (*bridgev2.UserInfo, error) { func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *bridgev2.Ghost, refreshAfter time.Duration) (*bridgev2.UserInfo, error) {
userID, err := signalid.ParseUserIDAsServiceID(ghost.ID) userID, err := signalid.ParseUserID(ghost.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -62,17 +59,12 @@ func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *b
// Don't do unnecessary fetches in background mode // Don't do unnecessary fetches in background mode
return nil, nil return nil, nil
} }
var contact *types.Recipient contact, err := s.Client.ContactByACIWithRefreshAfter(ctx, userID, refreshAfter)
if userID.Type == libsignalgo.ServiceIDTypePNI {
contact, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, uuid.Nil, userID.UUID, nil)
} else {
contact, err = s.Client.ContactByACIWithRefreshAfter(ctx, userID.UUID, refreshAfter)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
meta := ghost.Metadata.(*signalid.GhostMetadata) meta := ghost.Metadata.(*signalid.GhostMetadata)
if userID.Type != libsignalgo.ServiceIDTypePNI && (!s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt)) { if !s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt) {
return nil, nil return nil, nil
} }
return s.contactToUserInfo(ctx, contact) return s.contactToUserInfo(ctx, contact)
@ -165,41 +157,18 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
return ui, nil return ui, nil
} }
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool { func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
_, err := signalid.ParseUserIDAsServiceID(id) _, err := signalid.ParseUserIDAsServiceID(id)
return err == nil return err == nil
} }
func (s *SignalClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
parsedID, err := signalid.ParseUserIDAsServiceID(ghost.ID)
if err != nil {
return nil, err
}
resp, err := s.ResolveIdentifier(ctx, parsedID.String(), true)
if err != nil {
return nil, err
} else if resp == nil {
return nil, nil
}
resultID, err := signalid.ParseUserIDAsServiceID(resp.UserID)
if err != nil {
return nil, fmt.Errorf("failed to parse result user ID: %w", err)
}
if parsedID.Type == libsignalgo.ServiceIDTypePNI {
if resultID.Type == libsignalgo.ServiceIDTypeACI && !resultID.IsEmpty() {
resp.Chat.DMRedirectedTo = resp.UserID
} else {
resp.Chat.DMRedirectedTo = bridgev2.SpecialValueDMRedirectedToBot
}
}
return resp.Chat, nil
}
func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ bool) (*bridgev2.ResolveIdentifierResponse, error) {
var aci, pni uuid.UUID var aci, pni uuid.UUID
var e164Number uint64 var e164Number uint64
var recipient *types.Recipient var recipient *types.Recipient
serviceID, err := signalid.ParseUserIDAsServiceID(networkid.UserID(number)) serviceID, err := libsignalgo.ServiceIDFromString(number)
if err != nil { if err != nil {
number, err = bridgev2.CleanPhoneNumber(number) number, err = bridgev2.CleanPhoneNumber(number)
if err != nil { if err != nil {
@ -212,7 +181,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
e164String := fmt.Sprintf("+%d", e164Number) e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil { if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
return nil, fmt.Errorf("error looking up number in local contact list: %w", err) 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 aci = recipient.ACI
pni = recipient.PNI pni = recipient.PNI
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil { } else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
@ -228,9 +197,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") zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
} }
aci, pni = recipient.ACI, recipient.PNI aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
} }
} else { } else {
aci, pni = serviceID.ToACIAndPNI() aci, pni = serviceID.ToACIAndPNI()
@ -250,29 +216,31 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
return nil, fmt.Errorf("failed to convert contact: %w", err) return nil, fmt.Errorf("failed to convert contact: %w", err)
} }
var userID networkid.UserID
if aci != uuid.Nil {
userID = signalid.MakeUserID(aci)
} else {
userID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni))
}
// createChat is a no-op: chats don't need to be created, and we always return chat info // createChat is a no-op: chats don't need to be created, and we always return chat info
resp := &bridgev2.ResolveIdentifierResponse{ if aci != uuid.Nil {
UserID: userID, ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(aci))
UserInfo: userInfo, if err != nil {
Chat: s.makeCreateDMResponse(ctx, recipient, nil), return nil, fmt.Errorf("failed to get ghost: %w", err)
}
return &bridgev2.ResolveIdentifierResponse{
UserID: signalid.MakeUserID(aci),
UserInfo: userInfo,
Ghost: ghost,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}, nil
} else {
return &bridgev2.ResolveIdentifierResponse{
UserID: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
UserInfo: userInfo,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}, nil
} }
resp.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, resp.UserID)
if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err)
}
return resp, nil
} }
func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
group := &signalmeow.Group{ group := &signalmeow.Group{
Title: ptr.Val(params.Name).Name, Title: ptr.Val(params.Name).Name,
Members: make([]*signalmeow.GroupMember, 1, len(params.Participants)+1), Members: make([]*signalmeow.GroupMember, len(params.Participants)+1),
Description: ptr.Val(params.Topic).Topic, Description: ptr.Val(params.Topic).Topic,
AnnouncementsOnly: false, AnnouncementsOnly: false,
DisappearingMessagesDuration: uint32(ptr.Val(params.Disappear).Timer.Seconds()), DisappearingMessagesDuration: uint32(ptr.Val(params.Disappear).Timer.Seconds()),
@ -299,25 +267,14 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
ACI: s.Client.Store.ACI, ACI: s.Client.Store.ACI,
Role: signalmeow.GroupMember_ADMINISTRATOR, Role: signalmeow.GroupMember_ADMINISTRATOR,
} }
currentTS := uint64(time.Now().UnixMilli()) for i, member := range params.Participants {
for _, member := range params.Participants { userID, err := signalid.ParseUserID(member)
userID, err := signalid.ParseUserIDAsServiceID(member)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid user ID %q: %w", member, err) return nil, fmt.Errorf("invalid user ID %q: %w", member, err)
} }
if userID.Type == libsignalgo.ServiceIDTypeACI { group.Members[i+1] = &signalmeow.GroupMember{
group.Members = append(group.Members, &signalmeow.GroupMember{ ACI: userID,
ACI: userID.UUID, Role: signalmeow.GroupMember_DEFAULT, // TODO set proper role from power levels
Role: signalmeow.GroupMember_DEFAULT, // TODO set proper role from power levels
})
} else if userID.Type == libsignalgo.ServiceIDTypePNI {
// TODO check if this is correct
group.PendingMembers = append(group.PendingMembers, &signalmeow.PendingMember{
ServiceID: userID,
Role: signalmeow.GroupMember_DEFAULT,
AddedByUserID: s.Client.Store.ACI,
Timestamp: currentTS,
})
} }
} }
_, err := signalmeow.PrepareGroupCreation(group) _, err := signalmeow.PrepareGroupCreation(group)
@ -326,13 +283,13 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
} }
var avatarBytes []byte var avatarBytes []byte
var avatarMXC id.ContentURIString var avatarMXC id.ContentURIString
if params.Avatar != nil && params.Avatar.URL != "" { if params.Avatar != nil {
avatarMXC = params.Avatar.URL 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 { if err != nil {
return nil, fmt.Errorf("failed to download avatar: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to upload avatar: %w", err) return nil, fmt.Errorf("failed to upload avatar: %w", err)
} }
@ -362,7 +319,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
return nil, fmt.Errorf("failed to set portal room ID: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to create group: %w", err) return nil, fmt.Errorf("failed to create group: %w", err)
} }
@ -414,7 +371,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 { func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
namePtr := bridgev2.DefaultChatName name := ""
topic := PrivateChatTopic topic := PrivateChatTopic
selfUser := s.makeEventSender(s.Client.Store.ACI) selfUser := s.makeEventSender(s.Client.Store.ACI)
members := &bridgev2.ChatMemberList{ members := &bridgev2.ChatMemberList{
@ -441,7 +398,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
var serviceID libsignalgo.ServiceID var serviceID libsignalgo.ServiceID
var avatar *bridgev2.Avatar var avatar *bridgev2.Avatar
if recipient.ACI == uuid.Nil { if recipient.ACI == uuid.Nil {
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient)) name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI) serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else { } else {
if backupChat == nil { if backupChat == nil {
@ -453,7 +410,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
} }
members.OtherUserID = signalid.MakeUserID(recipient.ACI) members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI { if recipient.ACI == s.Client.Store.ACI {
namePtr = ptr.Ptr(NoteToSelfName) name = NoteToSelfName
avatar = &bridgev2.Avatar{ avatar = &bridgev2.Avatar{
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar), ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0, Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
@ -474,15 +431,15 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
return &bridgev2.CreateChatResponse{ return &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(serviceID), PortalKey: s.makeDMPortalKey(serviceID),
PortalInfo: &bridgev2.ChatInfo{ PortalInfo: &bridgev2.ChatInfo{
Name: namePtr, Name: &name,
Avatar: avatar, Avatar: avatar,
Topic: &topic, Topic: &topic,
Members: members, Members: members,
Type: ptr.Ptr(database.RoomTypeDM), Type: ptr.Ptr(database.RoomTypeDM),
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()), CanBackfill: backupChat != nil,
CanBackfill: backupChat != nil,
ExtraUpdates: updatePortalSyncMeta, ExtraUpdates: updatePortalSyncMeta,
}, },
} }
} }

View file

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

View file

@ -19,7 +19,6 @@ package connector
import ( import (
"context" "context"
"fmt" "fmt"
"sync/atomic"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -27,7 +26,6 @@ import (
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow"
@ -41,13 +39,11 @@ type SignalClient struct {
Ghost *bridgev2.Ghost Ghost *bridgev2.Ghost
queueEmptyWaiter *exsync.Event queueEmptyWaiter *exsync.Event
cancelChatSync atomic.Pointer[context.CancelFunc]
} }
var ( var (
_ bridgev2.NetworkAPI = (*SignalClient)(nil) _ bridgev2.NetworkAPI = (*SignalClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
) )
var pushCfg = &bridgev2.PushConfig{ 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) { func (s *SignalClient) LogoutRemote(ctx context.Context) {
if s.Client == nil { if s.Client == nil {
return return
} }
s.stopChatSync() err := s.Client.StopReceiveLoops()
err := s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
}
err = s.Client.StopReceiveLoops()
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout") 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) err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store") 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: case signalmeow.SignalConnectionEventLoggedOut:
s.stopChatSync()
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
if err == nil { if err == nil {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
@ -202,10 +188,6 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
} }
case signalmeow.SignalConnectionEventError: case signalmeow.SignalConnectionEventError:
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState")
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
case signalmeow.SignalConnectionEventFatalError:
s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState")
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()}) s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
@ -251,7 +233,7 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
case web.SignalWebsocketConnectionEventLoggedOut: case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Authed websocket logged out") log.Err(status.Err).Msg("Authed websocket logged out")
return fmt.Errorf("authed websocket logged out: %w", status.Err) return fmt.Errorf("authed websocket logged out: %w", status.Err)
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Authed websocket error") log.Err(status.Err).Msg("Authed websocket error")
return fmt.Errorf("authed websocket errored: %w", status.Err) return fmt.Errorf("authed websocket errored: %w", status.Err)
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
@ -265,7 +247,7 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
log.Err(status.Err).Msg("Unauthed websocket disconnected") log.Err(status.Err).Msg("Unauthed websocket disconnected")
case web.SignalWebsocketConnectionEventLoggedOut: case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Unauthed websocket logged out") log.Err(status.Err).Msg("Unauthed websocket logged out")
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Unauthed websocket error") log.Err(status.Err).Msg("Unauthed websocket error")
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
log.Info().Msg("Unauthed websocket clean shutdown") log.Info().Msg("Unauthed websocket clean shutdown")
@ -288,7 +270,6 @@ func (s *SignalClient) Disconnect() {
if s.Client == nil { if s.Client == nil {
return return
} }
s.stopChatSync()
err := s.Client.StopReceiveLoops() err := s.Client.StopReceiveLoops()
if err != nil { if err != nil {
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops") s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
@ -296,63 +277,45 @@ func (s *SignalClient) Disconnect() {
} }
func (s *SignalClient) postLoginConnect() { 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) 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) { func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, doSync bool) {
if ctx.Err() != nil { err := s.Client.RegisterCapabilities(ctx)
zerolog.Ctx(ctx).Debug(). if err != nil {
Int("retry_count", retryCount). zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
AnErr("ctx_err", ctx.Err()). } else {
Msg("Context is canceled, not trying to connect") zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
return
}
if retryCount == 0 {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
} }
ch, err := s.Client.StartReceiveLoops(ctx) ch, err := s.Client.StartReceiveLoops(ctx)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops") zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops")
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) if retryCount < 6 {
retryInSeconds := 2 << retryCount s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
if retryInSeconds > 150 { retryInSeconds := 2 << retryCount
retryInSeconds = 150 zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
} time.Sleep(time.Duration(retryInSeconds) * time.Second)
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection") s.tryConnect(ctx, retryCount+1, doSync)
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)()
}
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 { } else {
cancel() s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
if s.Client.Store.MasterKey != nil { }
go s.Client.SyncStorage(ctx) } else {
} go s.bridgeStateLoop(ch)
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"` NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"` LocationFormat string `yaml:"location_format"`
DisappearViewOnce bool `yaml:"disappear_view_once"` DisappearViewOnce bool `yaml:"disappear_view_once"`
ExtEvPolls bool `yaml:"extev_polls"`
displaynameTemplate *template.Template `yaml:"-"` 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, "note_to_self_avatar")
helper.Copy(up.Str, "location_format") helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "disappear_view_once") helper.Copy(up.Bool, "disappear_view_once")
helper.Copy(up.Bool, "extev_polls")
} }
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {

View file

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

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os" "io"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "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) return nil, fmt.Errorf("failed to parse direct media id: %w", err)
} }
var rawDataResp []byte
switch info := info.(type) { switch info := info.(type) {
case *signalid.DirectMediaAttachment: case *signalid.DirectMediaAttachment:
log.Info(). log.Info().
@ -42,15 +41,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
Uint32("size", info.Size). Uint32("size", info.Size).
Msg("Direct downloading attachment") Msg("Direct downloading attachment")
return &mediaproxy.GetMediaResponseFile{ return &mediaproxy.GetMediaResponseCallback{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { Callback: func(w io.Writer) (int64, error) {
_, err := signalmeow.DownloadAttachment( data, err := signalmeow.DownloadAttachment(
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size, w, ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size,
) )
if err != nil { 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 }, nil
case *signalid.DirectMediaGroupAvatar: 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) 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{
if err != nil { Callback: func(w io.Writer) (int64, error) {
log.Err(err).Msg("Direct download failed") data, err := client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
return nil, err if err != nil {
} log.Err(err).Msg("Direct download failed")
return 0, err
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
case *signalid.DirectMediaProfileAvatar: case *signalid.DirectMediaProfileAvatar:
log.Info(). log.Info().
Stringer("user_id", info.UserID). 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") return nil, fmt.Errorf("profile key not found")
} }
rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey) return &mediaproxy.GetMediaResponseCallback{
if err != nil { Callback: func(w io.Writer) (int64, error) {
log.Err(err).Msg("Direct download failed") data, err := client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
return nil, err if err != nil {
} log.Err(err).Msg("Direct download failed")
case *signalid.DirectMediaSticker: return 0, err
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) _, err = w.Write(data)
if err != nil { return int64(len(data)), err
log.Err(err).Msg("Direct download failed") },
return nil, err }, nil
}
default: default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info) 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' 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? # Should view-once messages disappear shortly after sending a read receipt on Matrix?
disappear_view_once: false 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) { 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 { if err != nil {
return nil, fmt.Errorf("failed to retrieve group by id: %w", err) return nil, fmt.Errorf("failed to retrieve group by id: %w", err)
} }
@ -123,30 +123,43 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members) applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink) joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink)
} }
for _, member := range groupInfo.RequestingMembers {
members.MemberMap.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, member := range groupInfo.PendingMembers {
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
MemberSender: s.makeEventSender(member.AddedByUserID),
})
}
for _, member := range groupInfo.Members { for _, member := range groupInfo.Members {
members.MemberMap.Set(bridgev2.ChatMember{ evtSender := s.makeEventSender(member.ACI)
EventSender: s.makeEventSender(member.ACI), members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) }
}
for _, member := range groupInfo.PendingMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
evtSender := s.makeEventSender(*aci)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
}
}
for _, member := range groupInfo.RequestingMembers {
evtSender := s.makeEventSender(member.ACI)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
Membership: event.MembershipKnock,
}
} }
for _, member := range groupInfo.BannedMembers { for _, member := range groupInfo.BannedMembers {
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{ aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
Membership: event.MembershipBan, if aci == nil {
}) continue
}
evtSender := s.makeEventSender(*aci)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
Membership: event.MembershipBan,
}
} }
if backupChat == nil { if backupChat == nil {
var err error var err error
@ -178,10 +191,6 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
}, nil }, nil
} }
func addMemberToMap(mc map[networkid.UserID]bridgev2.ChatMember, member bridgev2.ChatMember) {
mc[member.EventSender.Sender] = member
}
func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool { func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool {
meta := portal.Metadata.(*signalid.PortalMetadata) meta := portal.Metadata.(*signalid.PortalMetadata)
meta.LastSync = jsontime.UnixNow() meta.LastSync = jsontime.UnixNow()
@ -277,127 +286,131 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess), JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess),
} }
} }
mc := make(bridgev2.ChatMemberMap) var mc []bridgev2.ChatMember
for _, member := range groupChange.AddPendingMembers {
s.addChatMemberWithACIQuery(ctx, mc, member.ServiceID, bridgev2.ChatMember{
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
PrevMembership: event.MembershipLeave,
MemberSender: s.makeEventSender(member.AddedByUserID),
})
}
for _, member := range groupChange.AddRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, memberServiceID := range groupChange.DeletePendingMembers {
s.addChatMemberWithACIQuery(ctx, mc, *memberServiceID, bridgev2.ChatMember{
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
})
}
for _, memberACI := range groupChange.DeleteRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipKnock,
})
}
for _, memberACI := range groupChange.DeleteMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
})
}
for _, memberServiceID := range groupChange.DeleteBannedMembers {
s.addChatMemberWithACIQuery(ctx, mc, *memberServiceID, bridgev2.ChatMember{
Membership: event.MembershipLeave,
PrevMembership: event.MembershipBan,
})
}
for _, member := range groupChange.AddBannedMembers {
s.addChatMemberWithACIQuery(ctx, mc, member.ServiceID, bridgev2.ChatMember{
Membership: event.MembershipBan,
})
}
for _, member := range groupChange.PromotePendingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromotePendingPniAciMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
})
mc.Set(bridgev2.ChatMember{
EventSender: s.makePNIEventSender(member.PNI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true,
},
})
}
for _, member := range groupChange.PromoteRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipKnock,
})
}
for _, member := range groupChange.AddMembers { for _, member := range groupChange.AddMembers {
mc.Set(bridgev2.ChatMember{ mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) })
} }
for _, member := range groupChange.ModifyMemberRoles { for _, member := range groupChange.ModifyMemberRoles {
mc.Set(bridgev2.ChatMember{ mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) })
} }
bannedMembers := make(map[libsignalgo.ServiceID]bool)
for _, member := range groupChange.AddBannedMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
bannedMembers[member.ServiceID] = true
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipBan,
})
}
for _, memberACI := range groupChange.DeleteMembers {
if bannedMembers[libsignalgo.NewACIServiceID(*memberACI)] {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
})
}
for _, member := range groupChange.AddPendingMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
})
}
for _, memberServiceID := range groupChange.DeletePendingMembers {
if bannedMembers[*memberServiceID] {
continue
}
aci := s.maybeResolvePNItoACI(ctx, memberServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.AddRequestingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, memberACI := range groupChange.DeleteRequestingMembers {
if bannedMembers[libsignalgo.NewACIServiceID(*memberACI)] {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipKnock,
})
}
for _, memberServiceID := range groupChange.DeleteBannedMembers {
aci := s.maybeResolvePNItoACI(ctx, memberServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipBan,
})
}
for _, member := range groupChange.PromotePendingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromotePendingPniAciMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromoteRequestingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipKnock,
})
}
if len(mc) > 0 || pls != nil { if len(mc) > 0 || pls != nil {
ic.MemberChanges = &bridgev2.ChatMemberList{MemberMap: mc, PowerLevels: pls} ic.MemberChanges = &bridgev2.ChatMemberList{Members: mc, PowerLevels: pls}
} }
return ic, nil return ic, nil
} }
func (s *SignalClient) addChatMemberWithACIQuery( func (s *SignalClient) maybeResolvePNItoACI(ctx context.Context, serviceID *libsignalgo.ServiceID) *uuid.UUID {
ctx context.Context, mc bridgev2.ChatMemberMap, serviceID libsignalgo.ServiceID, member bridgev2.ChatMember, if serviceID.Type == libsignalgo.ServiceIDTypeACI {
) { return &serviceID.UUID
member.EventSender = s.makeEventSenderFromServiceID(serviceID)
mc.Set(member)
if aci := s.tryResolvePNItoLoggedInACI(ctx, serviceID); aci != nil {
member.EventSender = s.makeEventSender(*aci)
mc.Add(member)
} }
} device, err := s.Client.Store.DeviceStore.DeviceByPNI(ctx, serviceID.UUID)
if err != nil || device == nil {
func (s *SignalClient) tryResolvePNItoLoggedInACI(ctx context.Context, serviceID libsignalgo.ServiceID) *uuid.UUID {
if serviceID.Type != libsignalgo.ServiceIDTypePNI {
return nil return nil
} else if serviceID.UUID == s.Client.Store.PNI {
return &s.Client.Store.ACI
} else if s.Main.Bridge.Config.SplitPortals {
// When split portals is enabled, we don't care about anyone else's logins
return nil
} else if device, err := s.Client.Store.DeviceStore.DeviceByPNI(ctx, serviceID.UUID); err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ACI for PNI")
return nil
} else if device == nil {
return nil
} else {
return &device.ACI
} }
return &device.ACI
} }
func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) { func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) {
@ -430,8 +443,8 @@ func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal
chatInfoChange, err := s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(portal.ID), gc.GroupChange.Revision, gc.GroupChange) chatInfoChange, err := s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(portal.ID), gc.GroupChange.Revision, gc.GroupChange)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to convert group info") log.Err(err).Msg("Failed to convert group info")
} else { } else if gc.GroupChange.SourceServiceID.Type == libsignalgo.ServiceIDTypeACI {
portal.ProcessChatInfoChange(ctx, s.makeEventSenderFromServiceID(gc.GroupChange.SourceServiceID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts))) portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceServiceID.UUID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
} }
if gc.GroupChange.Revision == toRevision { if gc.GroupChange.Revision == toRevision {
break break

View file

@ -21,7 +21,6 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"slices"
"strconv" "strconv"
"time" "time"
@ -43,19 +42,16 @@ import (
) )
var ( var (
_ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DisappearTimerChangingNetworkAPI = (*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 { 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("failed_to_send_to_count", len(result.FailedToSendTo)).
Int("successfully_sent_to_count", len(result.SuccessfullySentTo)). Int("successfully_sent_to_count", len(result.SuccessfullySentTo)).
Logger() 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 { if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 {
log.Debug().Msg("No successes or failures - Probably sent to myself") log.Debug().Msg("No successes or failures - Probably sent to myself")
} else if len(result.SuccessfullySentTo) == 0 { } else if len(result.SuccessfullySentTo) == 0 {
log.Error().Msg("Failed to send event to all members of Signal group") 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") return errors.New("failed to send to any members of Signal group")
} else if len(result.SuccessfullySentTo) < totalRecipients { } else if len(result.SuccessfullySentTo) < totalRecipients {
if len(result.FailedToSendTo) > 0 { log.Warn().Msg("Only sent event to some members of Signal group")
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 { } else {
log.Debug().Msg("Sent event to all members of Signal group") 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) { 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( 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 { if err != nil {
return nil, err 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) msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.AddPendingToIgnore(networkid.TransactionID(msgID)) 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 { if err != nil {
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
} }
@ -145,7 +126,9 @@ func (s *SignalClient) doSendMessage(
ID: msgID, ID: msgID,
SenderID: signalid.MakeUserID(s.Client.Store.ACI), SenderID: signalid.MakeUserID(s.Client.Store.ACI),
Timestamp: time.UnixMilli(int64(ts)), Timestamp: time.UnixMilli(int64(ts)),
Metadata: meta, Metadata: &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
},
} }
return &bridgev2.MatrixMessageResponse{ return &bridgev2.MatrixMessageResponse{
DB: dbMsg, 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) 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 { if err != nil {
return err return err
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{EditMessage: &signalpb.EditMessage{
converted.Timestamp = &ts
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapEditMessage(&signalpb.EditMessage{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
DataMessage: converted, DataMessage: converted,
})) }})
if err != nil { if err != nil {
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
} }
prevID := msg.EditTarget.ID
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts) msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0} msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
msg.EditTarget.EditCount++ 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 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) { func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
return bridgev2.MatrixReactionPreResponse{ return bridgev2.MatrixReactionPreResponse{
SenderID: signalid.MakeUserID(s.Client.Store.ACI), 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) return nil, fmt.Errorf("failed to parse target message ID: %w", err)
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
Timestamp: proto.Uint64(ts), DataMessage: &signalpb.DataMessage{
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), Timestamp: proto.Uint64(ts),
Reaction: &signalpb.DataMessage_Reaction{ RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Emoji: proto.String(msg.PreHandleResp.Emoji), Reaction: &signalpb.DataMessage_Reaction{
Remove: proto.Bool(false), Emoji: proto.String(msg.PreHandleResp.Emoji),
TargetAuthorAciBinary: targetAuthorACI[:], Remove: proto.Bool(false),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}, },
})) }
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return nil, err 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) return fmt.Errorf("failed to parse target message ID: %w", err)
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
Timestamp: proto.Uint64(ts), DataMessage: &signalpb.DataMessage{
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), Timestamp: proto.Uint64(ts),
Reaction: &signalpb.DataMessage_Reaction{ RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Emoji: proto.String(msg.TargetReaction.Emoji), Reaction: &signalpb.DataMessage_Reaction{
Remove: proto.Bool(true), Emoji: proto.String(msg.TargetReaction.Emoji),
TargetAuthorAciBinary: targetAuthorACI[:], Remove: proto.Bool(true),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}, },
})) }
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return err return err
} }
@ -277,12 +234,15 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg
return fmt.Errorf("cannot delete other people's messages") return fmt.Errorf("cannot delete other people's messages")
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
Timestamp: proto.Uint64(ts), DataMessage: &signalpb.DataMessage{
Delete: &signalpb.DataMessage_Delete{ Timestamp: proto.Uint64(ts),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), Delete: &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}, },
})) }
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return err 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 { 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 { if err != nil {
return err 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 { if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI {
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
result := s.Client.SendMessage(ctx, userID, typingMessage) result := s.Client.SendMessage(ctx, userID, typingMessage)
if !result.WasSuccessful { if !result.WasSuccessful {
return result.Error return result.Error
} }
} else if groupID != "" {
_, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage)
if err != nil {
return err
}
} }
return nil 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) return false, fmt.Errorf("failed to download avatar: %w", err)
} }
avatarHash = sha256.Sum256(data) avatarHash = sha256.Sum256(data)
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "") avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to reupload avatar: %w", err) return false, fmt.Errorf("failed to reupload avatar: %w", err)
} }
@ -428,24 +385,22 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.
}, nil) }, nil)
} }
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
var targetIntent bridgev2.MatrixAPI var targetIntent bridgev2.MatrixAPI
var targetSignalID libsignalgo.ServiceID var targetSignalID uuid.UUID
var err error var err error
if msg.Portal.RoomType == database.RoomTypeDM { if msg.Portal.RoomType == database.RoomTypeDM {
//TODO: this probably needs to revert some changes and clean up the portal on leaves
switch msg.Type { switch msg.Type {
case bridgev2.Invite: case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm") return false, fmt.Errorf("cannot invite additional user to dm")
default: default:
return nil, nil return false, nil
} }
} }
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target) targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
if err != nil { 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) { switch target := msg.Target.(type) {
case *bridgev2.Ghost: case *bridgev2.Ghost:
@ -455,12 +410,12 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
if targetIntent == nil { if targetIntent == nil {
ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID)) ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
if err != nil { 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 targetIntent = ghost.Intent
} }
default: 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(). log := zerolog.Ctx(ctx).With().
Str("From Membership", string(msg.Type.From)). Str("From Membership", string(msg.Type.From)).
@ -479,35 +434,21 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
} }
switch msg.Type { switch msg.Type {
case bridgev2.AcceptInvite: case bridgev2.AcceptInvite:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept invite for non-ACI service ID")
}
gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{ gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{
ACI: targetSignalID.UUID, ACI: targetSignalID,
}} }}
case bridgev2.RevokeInvite, bridgev2.RejectInvite: case bridgev2.RevokeInvite, bridgev2.RejectInvite:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember}
case bridgev2.Leave, bridgev2.Kick: case bridgev2.Leave, bridgev2.Kick:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't kick non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.Invite: case bridgev2.Invite:
if targetSignalID.Type == libsignalgo.ServiceIDTypeACI { gc.AddMembers = []*signalmeow.AddMember{{
gc.AddMembers = []*signalmeow.AddMember{{ GroupMember: signalmeow.GroupMember{
GroupMember: signalmeow.GroupMember{ ACI: targetSignalID,
ACI: targetSignalID.UUID, Role: role,
Role: role, },
}, }}
}}
} else {
gc.AddPendingMembers = []*signalmeow.PendingMember{{
ServiceID: targetSignalID,
Role: role,
AddedByUserID: s.Client.Store.ACI,
Timestamp: uint64(msg.Event.Timestamp),
}}
}
// TODO: joining and knocking requires a way to obtain the invite link // TODO: joining and knocking requires a way to obtain the invite link
// because the joining/knocking member doesn't have the GroupMasterKey yet // because the joining/knocking member doesn't have the GroupMasterKey yet
// case bridgev2.Join: // case bridgev2.Join:
@ -524,59 +465,50 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
// Timestamp: uint64(time.Now().UnixMilli()), // Timestamp: uint64(time.Now().UnixMilli()),
// }} // }}
case bridgev2.AcceptKnock: case bridgev2.AcceptKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept knock from non-ACI service ID")
}
gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{ gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{
ACI: targetSignalID.UUID, ACI: targetSignalID,
Role: role, Role: role,
}} }}
case bridgev2.RetractKnock, bridgev2.RejectKnock: case bridgev2.RetractKnock, bridgev2.RejectKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID}
return nil, 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: case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft:
gc.AddBannedMembers = []*signalmeow.BannedMember{{ gc.AddBannedMembers = []*signalmeow.BannedMember{{
ServiceID: targetSignalID, ServiceID: libsignalgo.NewACIServiceID(targetSignalID),
Timestamp: uint64(time.Now().UnixMilli()), Timestamp: uint64(time.Now().UnixMilli()),
}} }}
switch msg.Type { switch msg.Type {
case bridgev2.BanJoined: case bridgev2.BanJoined:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanInvited: case bridgev2.BanInvited:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember}
case bridgev2.BanKnocked: case bridgev2.BanKnocked:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
} }
case bridgev2.Unban: case bridgev2.Unban:
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID} unbanUser := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&unbanUser}
default: 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) _, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil || groupID == "" { if err != nil || groupID == "" {
return nil, err return false, err
} }
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1 gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID) revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil { 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 {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
if err != nil { if err != nil {
return nil, err return false, err
} }
} }
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return nil, nil return true, nil
} }
func plToRole(pl int) signalmeow.GroupMemberRole { func plToRole(pl int) signalmeow.GroupMemberRole {
@ -608,17 +540,18 @@ func (s *SignalClient) HandleMatrixPowerLevels(ctx context.Context, msg *bridgev
if msg.Portal.RoomType == database.RoomTypeDM { if msg.Portal.RoomType == database.RoomTypeDM {
return false, nil return false, nil
} }
log := zerolog.Ctx(ctx)
gc := &signalmeow.GroupChange{} gc := &signalmeow.GroupChange{}
for _, plc := range msg.Users { for _, plc := range msg.Users {
if !hasAdminChanged(&plc.SinglePowerLevelChange) { if !hasAdminChanged(&plc.SinglePowerLevelChange) {
continue continue
} }
serviceID, err := signalid.ParseGhostOrUserLoginID(plc.Target) aci, err := signalid.ParseGhostOrUserLoginID(plc.Target)
if err != nil || serviceID.Type != libsignalgo.ServiceIDTypeACI { if err != nil {
continue log.Err(err).Msg("Couldn't parse user id")
} }
gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{ gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{
ACI: serviceID.UUID, ACI: aci,
Role: plToRole(plc.NewLevel), Role: plToRole(plc.NewLevel),
}) })
} }
@ -710,11 +643,13 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
}) })
} else { } else {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) 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{
Timestamp: ptr.Ptr(ts), DataMessage: &signalpb.DataMessage{
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)), Timestamp: ptr.Ptr(ts),
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
})) ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
},
})
if !res.WasSuccessful { if !res.WasSuccessful {
return false, res.Error return false, res.Error
} }
@ -722,231 +657,3 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
return true, nil 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" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exzerolog" "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"
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
@ -38,7 +35,6 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events" "go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types" "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
@ -56,8 +52,6 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
return s.handleSignalReadSelf(evt) return s.handleSignalReadSelf(evt)
case *events.DeleteForMe: case *events.DeleteForMe:
return s.handleSignalDeleteForMe(evt) return s.handleSignalDeleteForMe(evt)
case *events.MessageRequestResponse:
return s.handleSignalMessageRequestResponse(evt)
case *events.Call: case *events.Call:
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
case *events.ContactList: 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() { if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
content.MsgType = event.MsgText content.MsgType = event.MsgText
} }
content.BeeperActionMessage = &event.BeeperActionMessage{
Type: event.BeeperActionMessageCall,
}
} else { } else {
content.Body = "Call ended" content.Body = "Call ended"
} }
@ -176,7 +167,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
case *signalpb.DataMessage: case *signalpb.DataMessage:
switch { switch {
case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil, 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.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0: innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
return bridgev2.RemoteEventMessage return bridgev2.RemoteEventMessage
@ -185,7 +176,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
return bridgev2.RemoteEventReactionRemove return bridgev2.RemoteEventReactionRemove
} }
return bridgev2.RemoteEventReaction return bridgev2.RemoteEventReaction
case innerEvt.Delete != nil, innerEvt.AdminDelete != nil: case innerEvt.Delete != nil:
return bridgev2.RemoteEventMessageRemove return bridgev2.RemoteEventMessageRemove
case innerEvt.GetGroupV2().GetGroupChange() != nil: case innerEvt.GetGroupV2().GetGroupChange() != nil:
return bridgev2.RemoteEventChatInfoChange return bridgev2.RemoteEventChatInfoChange
@ -294,21 +285,16 @@ func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
} }
func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID { func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
var targetAuthorACI uuid.UUID var targetAuthorACI string
var targetSentTS uint64 var targetSentTS uint64
switch innerEvt := evt.Event.(type) { switch innerEvt := evt.Event.(type) {
case *signalpb.DataMessage: case *signalpb.DataMessage:
switch { switch {
case innerEvt.Reaction != nil: case innerEvt.Reaction != nil:
targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary()) targetAuthorACI = innerEvt.Reaction.GetTargetAuthorAci()
targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp() targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
case innerEvt.Delete != nil: case innerEvt.Delete != nil:
targetSentTS = innerEvt.Delete.GetTargetSentTimestamp() 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: default:
return "" return ""
} }
@ -317,10 +303,11 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
default: default:
return "" return ""
} }
if targetAuthorACI == uuid.Nil { targetAuthorUUID := evt.Info.Sender
targetAuthorACI = 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) { func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
@ -340,7 +327,7 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
if !ok { if !ok {
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event") 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 != "" { if converted.Disappear.Type != "" {
evtTS := evt.GetTimestamp() evtTS := evt.GetTimestamp()
if !dataMsg.GetIsViewOnce() { if !dataMsg.GetIsViewOnce() {
@ -359,52 +346,20 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
return converted, nil 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) { 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) editMsg, ok := evt.Event.(*signalpb.EditMessage)
if !ok { if !ok {
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event") 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? // 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? // TODO can anything other than the text be edited?
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1]) 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.EditCount++
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp()) editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
convertedEdit := &bridgev2.ConvertedEdit{ return &bridgev2.ConvertedEdit{
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart}, ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
} }, nil
if prevID != editPart.Part.ID {
convertedEdit.AddedParts = editStubMessage()
}
return convertedEdit, nil
} }
func (evt *Bv2ChatEvent) GetStreamOrder() int64 { func (evt *Bv2ChatEvent) GetStreamOrder() int64 {
@ -459,7 +414,7 @@ func (b *Bv2Receipt) GetReadUpTo() time.Time {
return 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 { 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) log := zerolog.Ctx(ctx)
@ -505,7 +460,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
Stringer("sender_id", evt.Sender). Stringer("sender_id", evt.Sender).
Stringer("receipt_type", evt.Content.GetType()). Stringer("receipt_type", evt.Content.GetType()).
Logger() 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) { 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)) 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(). log := s.UserLogin.Log.With().
Str("action", "handle signal read self"). Str("action", "handle signal read self").
Logger() 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -537,13 +492,6 @@ func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signa
return networkid.PortalKey{}, false return networkid.PortalKey{}, false
} }
return s.makeDMPortalKey(serviceID), true 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: case *signalpb.ConversationIdentifier_ThreadGroupId:
if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength { if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
log.Error(). log.Error().
@ -582,22 +530,6 @@ func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey net
return "" return ""
} }
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp()) 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: case *signalpb.AddressableMessage_AuthorE164:
log.Warn(). log.Warn().
Object("portal_key", portalKey). Object("portal_key", portalKey).
@ -688,45 +620,13 @@ func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
return true 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) { func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
log := s.UserLogin.Log.With(). log := s.UserLogin.Log.With().
Str("action", "handle aci found"). Str("action", "handle aci found").
Stringer("aci", evt.ACI). Stringer("aci", evt.ACI).
Stringer("pni", evt.PNI). Stringer("pni", evt.PNI).
Logger() Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx) ctx := log.WithContext(context.TODO())
pniPortalKey := s.makeDMPortalKey(evt.PNI) pniPortalKey := s.makeDMPortalKey(evt.PNI)
aciPortalKey := s.makeDMPortalKey(evt.ACI) aciPortalKey := s.makeDMPortalKey(evt.ACI)
result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey) 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) { func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger() 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 { for _, contact := range evt.Contacts {
if contact.ACI == uuid.Nil { if contact.ACI == uuid.Nil {
continue continue
@ -774,33 +674,6 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
if contact.ACI == s.Client.Store.ACI { if contact.ACI == s.Client.Store.ACI {
s.updateRemoteProfile(ctx, true) 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

@ -17,8 +17,6 @@
package connector package connector
import ( import (
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
@ -50,20 +48,3 @@ func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
Sender: signalid.MakeUserID(sender), Sender: signalid.MakeUserID(sender),
} }
} }
func (s *SignalClient) makePNIEventSender(sender uuid.UUID) bridgev2.EventSender {
return bridgev2.EventSender{
Sender: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(sender)),
}
}
func (s *SignalClient) makeEventSenderFromServiceID(serviceID libsignalgo.ServiceID) bridgev2.EventSender {
switch serviceID.Type {
case libsignalgo.ServiceIDTypeACI:
return s.makeEventSender(serviceID.UUID)
case libsignalgo.ServiceIDTypePNI:
return s.makePNIEventSender(serviceID.UUID)
default:
panic(fmt.Errorf("invalid service ID type %d", serviceID.Type))
}
}

View file

@ -18,12 +18,9 @@ package connector
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http"
"time" "time"
"github.com/coder/websocket"
"github.com/google/uuid" "github.com/google/uuid"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database" "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) { func (s *SignalConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
if flowID != "qr" { if flowID != "qr" {
return nil, bridgev2.ErrInvalidLoginFlowID return nil, fmt.Errorf("invalid login flow ID")
} }
return &QRLogin{User: user, Main: s}, nil return &QRLogin{User: user, Main: s}, nil
} }
@ -55,6 +52,8 @@ type QRLogin struct {
cancelChan context.CancelFunc cancelChan context.CancelFunc
ProvChan chan signalmeow.ProvisioningResponse ProvChan chan signalmeow.ProvisioningResponse
newQRCount int newQRCount int
ProvData *store.DeviceData
} }
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil) var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
@ -69,84 +68,16 @@ func (qr *QRLogin) Cancel() {
const ( const (
LoginStepQR = "fi.mau.signal.login.qr" LoginStepQR = "fi.mau.signal.login.qr"
LoginStepProcess = "fi.mau.signal.login.processing"
LoginStepComplete = "fi.mau.signal.login.complete" 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) { func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
log := qr.Main.Bridge.Log.With(). log := qr.Main.Bridge.Log.With().
Str("action", "login"). Str("action", "login").
Stringer("user_id", qr.User.MXID). Stringer("user_id", qr.User.MXID).
Logger() Logger()
provCtx, cancel := context.WithCancel(log.WithContext(qr.Main.Bridge.BackgroundCtx)) provCtx, cancel := context.WithCancel(log.WithContext(context.Background()))
qr.cancelChan = cancel qr.cancelChan = cancel
// Don't use the start context here: the channel will outlive the start request. // Don't use the start context here: the channel will outlive the start request.
qr.ProvChan = signalmeow.PerformProvisioning( qr.ProvChan = signalmeow.PerformProvisioning(
@ -156,16 +87,14 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
select { select {
case resp = <-qr.ProvChan: case resp = <-qr.ProvChan:
if resp.Err != nil { if resp.Err != nil {
return nil, wrapProvisioningError(resp.Err) return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningURLReceived { } else if resp.State != signalmeow.StateProvisioningURLReceived {
return nil, fmt.Errorf("unexpected state %v", resp.State) return nil, fmt.Errorf("unexpected state %v", resp.State)
} }
case <-ctx.Done(): case <-ctx.Done():
cancel() cancel()
if errors.Is(ctx.Err(), context.DeadlineExceeded) { return nil, ctx.Err()
return nil, ErrLoginTimedOut // TODO separate timeout here?
}
return nil, ErrLoginCancelled
} }
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait, Type: bridgev2.LoginStepTypeDisplayAndWait,
@ -183,11 +112,19 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
return nil, fmt.Errorf("login not started") 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 { select {
case resp := <-qr.ProvChan: case resp := <-qr.ProvChan:
if resp.Err != nil { if resp.Err != nil {
qr.cancelChan() qr.cancelChan()
return nil, wrapProvisioningError(resp.Err) return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningDataReceived { } else if resp.State != signalmeow.StateProvisioningDataReceived {
qr.cancelChan() qr.cancelChan()
return nil, fmt.Errorf("unexpected state %v", resp.State) 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() qr.cancelChan()
return nil, fmt.Errorf("no signal account ID received") 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 // 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. // 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.cancelChan()
qr.newQRCount++ qr.newQRCount++
if qr.newQRCount >= maxQRRefreshes { if qr.newQRCount >= 6 {
return nil, ErrLoginTimedOut return nil, fmt.Errorf("too many QR code refreshes")
} }
return qr.Start(ctx) return qr.Start(ctx)
case <-ctx.Done(): case <-ctx.Done():
qr.cancelChan() qr.cancelChan()
if errors.Is(ctx.Err(), context.DeadlineExceeded) { return nil, ctx.Err()
return nil, ErrLoginTimedOut
}
return nil, ErrLoginCancelled
} }
} }
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() 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{ ul, err := qr.User.NewLogin(ctx, &database.UserLogin{
ID: signalid.MakeUserLoginID(provData.ACI), ID: newLoginID,
RemoteName: provData.Number, RemoteName: qr.ProvData.Number,
RemoteProfile: status.RemoteProfile{ RemoteProfile: status.RemoteProfile{
Phone: provData.Number, Phone: qr.ProvData.Number,
}, },
Metadata: &signalid.UserLoginMetadata{}, Metadata: &signalid.UserLoginMetadata{},
}, &bridgev2.NewLoginParams{ }, &bridgev2.NewLoginParams{
@ -235,7 +190,7 @@ func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeComplete, Type: bridgev2.LoginStepTypeComplete,
StepID: LoginStepComplete, 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{ CompleteParams: &bridgev2.LoginCompleteParams{
UserLoginID: ul.ID, UserLoginID: ul.ID,
UserLogin: ul, UserLogin: ul,

View file

@ -20,18 +20,18 @@ package libsignalgo
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
*/ */
import "C" import "C"
import "runtime" import (
"runtime"
"unsafe"
)
type AccountEntropyPool string type AccountEntropyPool string
type SVRKey = fixedArray32
func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) { func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
var out SVRKey var out [C.SignalSVR_KEY_LEN]byte
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_account_entropy_pool_derive_svr_key( signalFfiError := C.signal_account_entropy_pool_derive_svr_key(
out.cFixedArray(), (*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
aepC, C.CString(string(aep)),
) )
runtime.KeepAlive(aep) runtime.KeepAlive(aep)
if signalFfiError != nil { if signalFfiError != nil {
@ -41,12 +41,10 @@ func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
} }
func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) { func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) {
var out BackupKey var out [C.SignalBACKUP_KEY_LEN]byte
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_account_entropy_pool_derive_backup_key( signalFfiError := C.signal_account_entropy_pool_derive_backup_key(
out.cFixedArray(), (*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
aepC, C.CString(string(aep)),
) )
runtime.KeepAlive(aep) runtime.KeepAlive(aep)
if signalFfiError != nil { 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) { func newAddress(name string, deviceID uint) (*Address, error) {
var pa C.SignalMutPointerProtocolAddress var pa C.SignalMutPointerProtocolAddress
nameStr, freeNameStr := GoStringToCString(name) signalFfiError := C.signal_address_new(&pa, C.CString(name), C.uint(deviceID))
defer freeNameStr()
signalFfiError := C.signal_address_new(&pa, nameStr, C.uint(deviceID))
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) return nil, wrapError(signalFfiError)
} }
@ -83,7 +81,7 @@ func (pa *Address) CancelFinalizer() {
} }
func (pa *Address) Name() (string, error) { func (pa *Address) Name() (string, error) {
var name C.SignalCStringPtr var name *C.char
signalFfiError := C.signal_address_get_name(&name, pa.constPtr()) signalFfiError := C.signal_address_get_name(&name, pa.constPtr())
runtime.KeepAlive(pa) runtime.KeepAlive(pa)
if signalFfiError != nil { if signalFfiError != nil {

View file

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

View file

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

View file

@ -21,8 +21,6 @@ package libsignalgo
*/ */
import "C" import "C"
import ( import (
"fmt"
"runtime"
"unsafe" "unsafe"
) )
@ -44,22 +42,6 @@ func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
return buf 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 { func EmptyBorrowedBuffer() C.SignalBorrowedBuffer {
return C.SignalBorrowedBuffer{} return C.SignalBorrowedBuffer{}
} }

View file

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

View file

@ -18,20 +18,12 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
#include <stdlib.h>
*/ */
import "C" import "C"
import "unsafe" import "unsafe"
func GoStringToCString(str string) (C.SignalCStringPtr, func()) { func CopyCStringToString(cString *C.char) (s string) {
cStr := C.CString(str) s = C.GoString(cString)
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)))
C.signal_free_string(cString) C.signal_free_string(cString)
return return
} }
@ -47,17 +39,3 @@ func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
C.signal_free_buffer(buffer.base, buffer.length) C.signal_free_buffer(buffer.base, buffer.length)
return 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 "C"
import ( import (
"runtime" "runtime"
"time"
) )
type DecryptionErrorMessage struct { type DecryptionErrorMessage struct {
@ -48,7 +49,7 @@ func DeserializeDecryptionErrorMessage(messageBytes []byte) (*DecryptionErrorMes
return wrapDecryptionErrorMessage(dem.raw), nil 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 var dem C.SignalMutPointerDecryptionErrorMessage
signalFfiError := C.signal_decryption_error_message_for_original_message( signalFfiError := C.signal_decryption_error_message_for_original_message(
&dem, &dem,
@ -111,14 +112,14 @@ func (dem *DecryptionErrorMessage) Serialize() ([]byte, error) {
return CopySignalOwnedBufferToBytes(serialized), nil return CopySignalOwnedBufferToBytes(serialized), nil
} }
func (dem *DecryptionErrorMessage) GetTimestamp() (uint64, error) { func (dem *DecryptionErrorMessage) GetTimestamp() (time.Time, error) {
var ts C.uint64_t var ts C.uint64_t
signalFfiError := C.signal_decryption_error_message_get_timestamp(&ts, dem.constPtr()) signalFfiError := C.signal_decryption_error_message_get_timestamp(&ts, dem.constPtr())
runtime.KeepAlive(dem) runtime.KeepAlive(dem)
if signalFfiError != nil { 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) { 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) { func (dtk *DeviceTransferKey) GenerateCertificate(name string, days int) ([]byte, error) {
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{} var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
nameStr, freeNameStr := GoStringToCString(name) signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), C.CString(name), C.uint32_t(days))
defer freeNameStr()
signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), nameStr, C.uint32_t(days))
runtime.KeepAlive(dtk) runtime.KeepAlive(dtk)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) return nil, wrapError(signalFfiError)

View file

@ -26,91 +26,34 @@ import (
type ErrorCode int type ErrorCode int
func (e ErrorCode) Error() string {
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
}
const ( const (
ErrorCodeUnknownError ErrorCode = 1 ErrorCodeUnknownError ErrorCode = 1
ErrorCodeInvalidState ErrorCode = 2 ErrorCodeInvalidState ErrorCode = 2
ErrorCodeInternalError ErrorCode = 3 ErrorCodeInternalError ErrorCode = 3
ErrorCodeNullParameter ErrorCode = 4 ErrorCodeNullParameter ErrorCode = 4
ErrorCodeInvalidArgument ErrorCode = 5 ErrorCodeInvalidArgument ErrorCode = 5
ErrorCodeInvalidType ErrorCode = 6 ErrorCodeInvalidType ErrorCode = 6
ErrorCodeInvalidUtf8String ErrorCode = 7 ErrorCodeInvalidUtf8String ErrorCode = 7
ErrorCodeCancelled ErrorCode = 8 ErrorCodeProtobufError ErrorCode = 10
ErrorCodeProtobufError ErrorCode = 10 ErrorCodeLegacyCiphertextVersion ErrorCode = 21
ErrorCodeLegacyCiphertextVersion ErrorCode = 21 ErrorCodeUnknownCiphertextVersion ErrorCode = 22
ErrorCodeUnknownCiphertextVersion ErrorCode = 22 ErrorCodeUnrecognizedMessageVersion ErrorCode = 23
ErrorCodeUnrecognizedMessageVersion ErrorCode = 23 ErrorCodeInvalidMessage ErrorCode = 30
ErrorCodeInvalidMessage ErrorCode = 30 ErrorCodeSealedSenderSelfSend ErrorCode = 31
ErrorCodeSealedSenderSelfSend ErrorCode = 31 ErrorCodeInvalidKey ErrorCode = 40
ErrorCodeInvalidKey ErrorCode = 40 ErrorCodeInvalidSignature ErrorCode = 41
ErrorCodeInvalidSignature ErrorCode = 41 ErrorCodeInvalidAttestationData ErrorCode = 42
ErrorCodeInvalidAttestationData ErrorCode = 42 ErrorCodeFingerprintVersionMismatch ErrorCode = 51
ErrorCodeFingerprintVersionMismatch ErrorCode = 51 ErrorCodeFingerprintParsingError ErrorCode = 52
ErrorCodeFingerprintParsingError ErrorCode = 52 ErrorCodeUntrustedIdentity ErrorCode = 60
ErrorCodeUntrustedIdentity ErrorCode = 60 ErrorCodeInvalidKeyIdentifier ErrorCode = 70
ErrorCodeInvalidKeyIdentifier ErrorCode = 70 ErrorCodeSessionNotFound ErrorCode = 80
ErrorCodeSessionNotFound ErrorCode = 80 ErrorCodeInvalidRegistrationId ErrorCode = 81
ErrorCodeInvalidRegistrationId ErrorCode = 81 ErrorCodeInvalidSession ErrorCode = 82
ErrorCodeInvalidSession ErrorCode = 82 ErrorCodeInvalidSenderKeySession ErrorCode = 83
ErrorCodeInvalidSenderKeySession ErrorCode = 83 ErrorCodeDuplicatedMessage ErrorCode = 90
ErrorCodeInvalidProtocolAddress ErrorCode = 84 ErrorCodeCallbackError ErrorCode = 100
ErrorCodeDuplicatedMessage ErrorCode = 90 ErrorCodeVerificationFailure ErrorCode = 110
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 { type SignalError struct {
@ -122,10 +65,6 @@ func (e *SignalError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message) 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 { func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error {
if signalError == nil { if signalError == nil {
return nil return nil
@ -152,7 +91,7 @@ func wrapError(signalError *C.SignalFfiError) error {
} }
func wrapSignalError(signalError *C.SignalFfiError, errorType C.uint32_t) 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) getMessageError := C.signal_error_get_message(&messageBytes, signalError)
if getMessageError != nil { if getMessageError != nil {
// Ignore any errors from this, it will just end up being an empty string. // 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) { func (f *Fingerprint) DisplayString() (string, error) {
var displayString C.SignalCStringPtr var displayString *C.char
signalFfiError := C.signal_fingerprint_display_string(&displayString, f.constPtr()) signalFfiError := C.signal_fingerprint_display_string(&displayString, f.constPtr())
runtime.KeepAlive(f) runtime.KeepAlive(f)
if signalFfiError != nil { 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( signalFfiError := C.signal_group_encrypt_message(
&ciphertextMessage, &ciphertextMessage,
sender.constPtr(), sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)), (*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
BytesToBuffer(ptext), BytesToBuffer(ptext),
callbackCtx.wrapSenderKeyStore(store)) callbackCtx.wrapSenderKeyStore(store))
runtime.KeepAlive(ptext) runtime.KeepAlive(ptext)

View file

@ -31,9 +31,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const RandomnessLength = 32 type Randomness [C.SignalRANDOMNESS_LEN]byte
type Randomness = fixedArray32
func GenerateRandomness() Randomness { func GenerateRandomness() Randomness {
var randomness Randomness var randomness Randomness
@ -44,39 +42,14 @@ func GenerateRandomness() Randomness {
return randomness return randomness
} }
const GroupMasterKeyLength = 32 const GroupMasterKeyLength = C.SignalGROUP_MASTER_KEY_LEN
const GroupIdentifierLength = 32 const GroupIdentifierLength = C.SignalGROUP_IDENTIFIER_LEN
const GroupSecretParamsLength = 289
type GroupMasterKey [GroupMasterKeyLength]byte type GroupMasterKey [GroupMasterKeyLength]byte
type GroupSecretParams [GroupSecretParamsLength]byte type GroupSecretParams [C.SignalGROUP_SECRET_PARAMS_LEN]byte
type GroupPublicParams = fixedArray97 type GroupPublicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]byte
type GroupIdentifier [GroupIdentifierLength]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 { func (gid *GroupIdentifier) String() string {
if gid == nil { if gid == nil {
return "" return ""
@ -84,8 +57,8 @@ func (gid *GroupIdentifier) String() string {
return base64.StdEncoding.EncodeToString(gid[:]) return base64.StdEncoding.EncodeToString(gid[:])
} }
type UUIDCiphertext = fixedArray65 type UUIDCiphertext [C.SignalUUID_CIPHERTEXT_LEN]byte
type ProfileKeyCiphertext = fixedArray65 type ProfileKeyCiphertext [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]byte
func GenerateGroupSecretParams() (GroupSecretParams, error) { func GenerateGroupSecretParams() (GroupSecretParams, error) {
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness()) 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) { func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
var params GroupSecretParams var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_generate_deterministic(params.cFixedArray(), randomness.cConstFixedArray()) signalFfiError := C.signal_group_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
runtime.KeepAlive(randomness) runtime.KeepAlive(randomness)
if signalFfiError != nil { if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError) 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) { func DeriveGroupSecretParamsFromMasterKey(groupMasterKey GroupMasterKey) (GroupSecretParams, error) {
var params GroupSecretParams var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_derive_from_master_key(params.cFixedArray(), groupMasterKey.cConstFixedArray()) 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) runtime.KeepAlive(groupMasterKey)
if signalFfiError != nil { if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError) 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) { func (gsp *GroupSecretParams) GetPublicParams() (*GroupPublicParams, error) {
var publicParams GroupPublicParams var publicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_get_public_params(publicParams.cFixedArray(), gsp.cConstFixedArray()) signalFfiError := C.signal_group_secret_params_get_public_params(&publicParams, (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)))
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { func GetGroupIdentifier(groupPublicParams GroupPublicParams) (*GroupIdentifier, error) {
var groupIdentifier GroupIdentifier var groupIdentifier [C.SignalGROUP_IDENTIFIER_LEN]C.uchar
signalFfiError := C.signal_group_public_params_get_group_identifier(groupIdentifier.cFixedArray(), groupPublicParams.cConstFixedArray()) signalFfiError := C.signal_group_public_params_get_group_identifier(&groupIdentifier, (*[C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(&groupPublicParams)))
runtime.KeepAlive(groupPublicParams) runtime.KeepAlive(groupPublicParams)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error) {
@ -152,7 +129,7 @@ func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error
borrowedBlob := BytesToBuffer(blob) borrowedBlob := BytesToBuffer(blob)
signalFfiError := C.signal_group_secret_params_decrypt_blob_with_padding( signalFfiError := C.signal_group_secret_params_decrypt_blob_with_padding(
&plaintext, &plaintext,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
borrowedBlob, borrowedBlob,
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
@ -168,8 +145,8 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
borrowedPlaintext := BytesToBuffer(plaintext) borrowedPlaintext := BytesToBuffer(plaintext)
signalFfiError := C.signal_group_secret_params_encrypt_blob_with_padding_deterministic( signalFfiError := C.signal_group_secret_params_encrypt_blob_with_padding_deterministic(
&ciphertext, &ciphertext,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
randomness.cConstFixedArray(), (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
borrowedPlaintext, borrowedPlaintext,
(C.uint32_t)(padding_len), (C.uint32_t)(padding_len),
) )
@ -184,11 +161,11 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
} }
func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) { func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
var serviceIDBytes ServiceIDFixedBytes u := C.SignalServiceIdFixedWidthBinaryBytes{}
signalFfiError := C.signal_group_secret_params_decrypt_service_id( signalFfiError := C.signal_group_secret_params_decrypt_service_id(
serviceIDBytes.cFixedArray(), &u,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
ciphertextServiceID.cConstFixedArray(), (*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextServiceID) runtime.KeepAlive(ciphertextServiceID)
@ -196,31 +173,33 @@ func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertex
return EmptyServiceID, wrapError(signalFfiError) return EmptyServiceID, wrapError(signalFfiError)
} }
serviceID := ServiceIDFromCFixedBytes(serviceIDBytes.cFixedArray()) serviceID := ServiceIDFromCFixedBytes(&u)
return serviceID, nil return serviceID, nil
} }
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) { 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( signalFfiError := C.signal_group_secret_params_encrypt_service_id(
cipherTextServiceID.cFixedArray(), &cipherTextServiceID,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
serviceID.cConstFixedArray(), serviceID.CFixedBytes(),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { 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( signalFfiError := C.signal_group_secret_params_decrypt_profile_key(
profileKey.cFixedArray(), &profileKey,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
ciphertextProfileKey.cConstFixedArray(), (*[C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextProfileKey)),
NewACIServiceID(u).cConstFixedArray(), NewACIServiceID(u).CFixedBytes(),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextProfileKey) runtime.KeepAlive(ciphertextProfileKey)
@ -228,23 +207,27 @@ func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyC
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { 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( signalFfiError := C.signal_group_secret_params_encrypt_profile_key(
ciphertextProfileKey.cFixedArray(), &ciphertextProfileKey,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
profileKey.cConstFixedArray(), (*[C.SignalPROFILE_KEY_LEN]C.uint8_t)(unsafe.Pointer(&profileKey)),
NewACIServiceID(u).cConstFixedArray(), NewACIServiceID(u).CFixedBytes(),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
runtime.KeepAlive(profileKey) runtime.KeepAlive(profileKey)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { 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( signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
&out, &out,
C.SignalConstPointerServerPublicParams{spp}, C.SignalConstPointerServerPublicParams{spp},
randomness.cConstFixedArray(), (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
credential.cConstFixedArray(), (*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar)(unsafe.Pointer(&credential)),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
runtime.KeepAlive(credential) runtime.KeepAlive(credential)
@ -269,14 +252,16 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
} }
func (gsp *GroupSecretParams) GetMasterKey() (*GroupMasterKey, error) { 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( signalFfiError := C.signal_group_secret_params_get_master_key(
masterKey.cFixedArray(), &masterKeyBytes,
gsp.cConstFixedArray(), (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
) )
runtime.KeepAlive(gsp) runtime.KeepAlive(gsp)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) 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) { 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 { type IdentityKeyPair struct {
@ -113,13 +114,14 @@ func GenerateIdentityKeyPair() (*IdentityKeyPair, error) {
} }
func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) { func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) {
var keys C.SignalPairOfMutPointerPublicKeyMutPointerPrivateKey var privateKey C.SignalMutPointerPrivateKey
signalFfiError := C.signal_identitykeypair_deserialize(&keys, BytesToBuffer(bytes)) var publicKey C.SignalMutPointerPublicKey
signalFfiError := C.signal_identitykeypair_deserialize(&privateKey, &publicKey, BytesToBuffer(bytes))
runtime.KeepAlive(bytes) runtime.KeepAlive(bytes)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) return nil, wrapError(signalFfiError)
} }
return &IdentityKeyPair{publicKey: wrapPublicKey(keys.first.raw), privateKey: wrapPrivateKey(keys.second.raw)}, nil return &IdentityKeyPair{publicKey: wrapPublicKey(publicKey.raw), privateKey: wrapPrivateKey(privateKey.raw)}, nil
} }
func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*IdentityKeyPair, error) { func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*IdentityKeyPair, error) {

View file

@ -20,12 +20,14 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #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_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_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, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address); 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, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction); extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
*/ */
import "C" import "C"
import ( import (
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
} }
//export signal_get_identity_key_pair_callback //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 { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
key, err := store.GetIdentityKeyPair(ctx) key, err := store.GetIdentityKeyPair(ctx)
if err != nil { if err != nil {
return err return err
} }
if key == nil { if key == nil {
keyp.first.raw = nil *keyp = nil
keyp.second.raw = nil } else {
return nil clone, err := key.privateKey.Clone()
if err != nil {
return err
}
clone.CancelFinalizer()
*keyp = clone.ptr
} }
privClone, err := key.privateKey.Clone()
if err != nil {
return err
}
pubClone, err := key.publicKey.Clone()
if err != nil {
return err
}
privClone.CancelFinalizer()
pubClone.CancelFinalizer()
keyp.first.raw = privClone.ptr
keyp.second.raw = pubClone.ptr
return err 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 //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 { func signal_save_identity_key_callback(storeCtx unsafe.Pointer, address *C.const_address, publicKey *C.const_public_key) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
publicKeyStruct := PublicKey{ptr: publicKey.raw} publicKeyStruct := PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(publicKey))}
cloned, err := publicKeyStruct.Clone() cloned, err := publicKeyStruct.Clone()
if err != nil { 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() theirServiceID, err := addr.NameServiceID()
if err != nil { if err != nil {
return err return -1, err
} }
replaced, err := store.SaveIdentityKey( replaced, err := store.SaveIdentityKey(
ctx, ctx,
@ -106,21 +101,20 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t,
&IdentityKey{cloned}, &IdentityKey{cloned},
) )
if err != nil { if err != nil {
return err return -1, err
} }
if replaced { if replaced {
*out = 1 return 1, nil
} else { } else {
*out = 0 return 0, nil
} }
return nil
}) })
} }
//export signal_get_identity_key_callback //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 { 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() theirServiceID, err := addr.NameServiceID()
if err != nil { if err != nil {
return err 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) key, err := store.GetIdentityKey(ctx, theirServiceID)
if err == nil && key != nil { if err == nil && key != nil {
key.publicKey.CancelFinalizer() key.publicKey.CancelFinalizer()
public_keyp.raw = key.publicKey.ptr *public_keyp = key.publicKey.ptr
} }
return err return err
}) })
} }
//export signal_is_trusted_identity_callback //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 { 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 wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
addr := &Address{ptr: address.raw} addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID() theirServiceID, err := addr.NameServiceID()
if err != nil { 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 { 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 { func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct {
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{ return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback), get_identity_key_pair: C.SignalGetIdentityKeyPair(C.signal_get_identity_key_pair_callback),
get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback), get_local_registration_id: C.SignalGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback), save_identity: C.SignalSaveIdentityKey(C.signal_save_identity_key_callback),
save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback), get_identity: C.SignalGetIdentityKey(C.signal_get_identity_key_callback),
is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback), is_trusted_identity: C.SignalIsTrustedIdentity(C.signal_is_trusted_identity_callback),
destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
}} }}
} }

View file

@ -20,10 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id); typedef const SignalKyberPreKeyRecord const_kyber_pre_key_record;
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 int signal_load_kyber_pre_key_callback(void *store_ctx, SignalKyberPreKeyRecord **recordp, uint32_t id);
extern void signal_destroy_kyber_pre_key_store_callback(void *store_ctx); 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 "C"
import ( import (
@ -38,21 +39,21 @@ type KyberPreKeyStore interface {
} }
//export signal_load_kyber_pre_key_callback //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 { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
key, err := store.LoadKyberPreKey(ctx, uint32(id)) key, err := store.LoadKyberPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_kyber_pre_key_callback //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 { 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() cloned, err := record.Clone()
if err != nil { if err != nil {
return err 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 //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 { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
// TODO use ecPrekeyID and baseKey? err := store.MarkKyberPreKeyUsed(ctx, uint32(id))
return 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 { func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{ return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_kyber_pre_key: C.SignalFfiKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback), load_kyber_pre_key: C.SignalLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
store_kyber_pre_key: C.SignalFfiKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback), store_kyber_pre_key: C.SignalStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
mark_kyber_pre_key_used: C.SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback), mark_kyber_pre_key_used: C.SignalMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
destroy: C.SignalFfiKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
}} }}
} }

@ -1 +1 @@
Subproject commit 857c4dca03537dc5e395a5e1eda6bf18f59c3601 Subproject commit 43a23efa1118ac32a1434ab317025adfa2b91e4a

File diff suppressed because it is too large Load diff

View file

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

View file

@ -27,7 +27,7 @@ import (
"time" "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 ciphertextMessage C.SignalMutPointerCiphertextMessage
var now C.uint64_t = C.uint64_t(time.Now().Unix()) var now C.uint64_t = C.uint64_t(time.Now().Unix())
callbackCtx := NewCallbackContext(ctx) callbackCtx := NewCallbackContext(ctx)
@ -36,7 +36,6 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
&ciphertextMessage, &ciphertextMessage,
BytesToBuffer(plaintext), BytesToBuffer(plaintext),
forAddress.constPtr(), forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityKeyStore), callbackCtx.wrapIdentityKeyStore(identityKeyStore),
now, now,
@ -49,7 +48,7 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
return wrapCiphertextMessage(ciphertextMessage.raw), nil 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) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{} var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -57,7 +56,6 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
&decrypted, &decrypted,
message.constPtr(), message.constPtr(),
fromAddress.constPtr(), fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
) )
@ -71,10 +69,10 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
type Message struct { type Message struct {
nc noCopy nc noCopy
ptr *C.SignalSignalMessage ptr *C.SignalMessage
} }
func wrapMessage(ptr *C.SignalSignalMessage) *Message { func wrapMessage(ptr *C.SignalMessage) *Message {
message := &Message{ptr: ptr} message := &Message{ptr: ptr}
runtime.SetFinalizer(message, (*Message).Destroy) runtime.SetFinalizer(message, (*Message).Destroy)
return message return message
@ -156,3 +154,22 @@ func (m *Message) GetCounter() (uint32, error) {
} }
return uint32(counter), nil 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" #include "./libsignal-ffi.h"
*/ */
import "C" import "C"
import "runtime" import (
"runtime"
"unsafe"
)
type MessageBackupKey struct { type MessageBackupKey struct {
nc noCopy nc noCopy
ptr *C.SignalMessageBackupKey ptr *C.SignalMessageBackupKey
} }
const MessageBackupKeyBytesLength = 32
type messageBackupKeyBytes = fixedArray32
func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey { func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
backupKey := &MessageBackupKey{ptr: ptr} backupKey := &MessageBackupKey{ptr: ptr}
runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy) runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy)
@ -39,12 +38,10 @@ func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceID) (*MessageBackupKey, error) { func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceID) (*MessageBackupKey, error) {
var bk C.SignalMutPointerMessageBackupKey var bk C.SignalMutPointerMessageBackupKey
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_message_backup_key_from_account_entropy_pool( signalFfiError := C.signal_message_backup_key_from_account_entropy_pool(
&bk, &bk,
aepC, C.CString(string(aep)),
aci.cConstFixedArray(), aci.CFixedBytes(),
nil, // TODO what's a forward secrecy token? nil, // TODO what's a forward secrecy token?
) )
runtime.KeepAlive(aep) runtime.KeepAlive(aep)
@ -58,8 +55,8 @@ func MessageBackupKeyFromBackupKeyAndID(backupKey *BackupKey, backupID *BackupID
var bk C.SignalMutPointerMessageBackupKey var bk C.SignalMutPointerMessageBackupKey
signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id( signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id(
&bk, &bk,
backupKey.cConstFixedArray(), (*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(backupKey)),
backupID.cConstFixedArray(), (*[BackupIDLength]C.uint8_t)(unsafe.Pointer(backupID)),
nil, // TODO what's a forward secrecy token? nil, // TODO what's a forward secrecy token?
) )
runtime.KeepAlive(backupKey) runtime.KeepAlive(backupKey)
@ -83,26 +80,26 @@ func (bk *MessageBackupKey) Destroy() error {
return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr())) return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr()))
} }
func (bk *MessageBackupKey) GetHMACKey() ([MessageBackupKeyBytesLength]byte, error) { func (bk *MessageBackupKey) GetHMACKey() ([32]byte, error) {
var out messageBackupKeyBytes var out [32]byte
signalFfiError := C.signal_message_backup_key_get_hmac_key( signalFfiError := C.signal_message_backup_key_get_hmac_key(
out.cFixedArray(), (*[32]C.uint8_t)(unsafe.Pointer(&out)),
bk.constPtr(), bk.constPtr(),
) )
if signalFfiError != nil { 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) { func (bk *MessageBackupKey) GetAESKey() ([32]byte, error) {
var out messageBackupKeyBytes var out [32]byte
signalFfiError := C.signal_message_backup_key_get_aes_key( signalFfiError := C.signal_message_backup_key_get_aes_key(
out.cFixedArray(), (*[32]C.uint8_t)(unsafe.Pointer(&out)),
bk.constPtr(), bk.constPtr(),
) )
if signalFfiError != nil { 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" "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) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{} var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -34,12 +34,12 @@ func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddres
&decrypted, &decrypted,
preKeyMessage.constPtr(), preKeyMessage.constPtr(),
fromAddress.constPtr(), fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
callbackCtx.wrapPreKeyStore(preKeyStore), callbackCtx.wrapPreKeyStore(preKeyStore),
callbackCtx.wrapSignedPreKeyStore(signedPreKeyStore), callbackCtx.wrapSignedPreKeyStore(signedPreKeyStore),
callbackCtx.wrapKyberPreKeyStore(kyberPreKeyStore), callbackCtx.wrapKyberPreKeyStore(kyberPreKeyStore),
false, // no pq ratchets yet
) )
runtime.KeepAlive(preKeyMessage) runtime.KeepAlive(preKeyMessage)
runtime.KeepAlive(fromAddress) runtime.KeepAlive(fromAddress)

View file

@ -27,17 +27,17 @@ import (
"time" "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) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var now C.uint64_t = C.uint64_t(time.Now().Unix()) var now C.uint64_t = C.uint64_t(time.Now().Unix())
signalFfiError := C.signal_process_prekey_bundle( signalFfiError := C.signal_process_prekey_bundle(
bundle.constPtr(), bundle.constPtr(),
forAddress.constPtr(), forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
now, now,
false, // no pq ratchets yet
) )
runtime.KeepAlive(bundle) runtime.KeepAlive(bundle)
runtime.KeepAlive(forAddress) runtime.KeepAlive(forAddress)

View file

@ -20,10 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_pre_key_callback(void *store_ctx, SignalMutPointerPreKeyRecord *recordp, uint32_t id); typedef const SignalPreKeyRecord const_pre_key_record;
extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerPreKeyRecord 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 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 "C"
import ( import (
@ -38,21 +39,21 @@ type PreKeyStore interface {
} }
//export signal_load_pre_key_callback //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 { return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
key, err := store.LoadPreKey(ctx, uint32(id)) key, err := store.LoadPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_pre_key_callback //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 { 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() cloned, err := record.Clone()
if err != nil { if err != nil {
return err 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 { func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct {
return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{ return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_pre_key: C.SignalFfiPreKeyStoreLoadPreKey(C.signal_load_pre_key_callback), load_pre_key: C.SignalLoadPreKey(C.signal_load_pre_key_callback),
store_pre_key: C.SignalFfiPreKeyStoreStorePreKey(C.signal_store_pre_key_callback), store_pre_key: C.SignalStorePreKey(C.signal_store_pre_key_callback),
remove_pre_key: C.SignalFfiPreKeyStoreRemovePreKey(C.signal_remove_pre_key_callback), remove_pre_key: C.SignalRemovePreKey(C.signal_remove_pre_key_callback),
destroy: C.SignalFfiPreKeyStoreDestroy(C.signal_destroy_pre_key_store_callback),
}} }}
} }

View file

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

View file

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

View file

@ -44,14 +44,10 @@ func wrapSenderCertificate(ptr *C.SignalSenderCertificate) *SenderCertificate {
// the Swift bindings). // the Swift bindings).
func NewSenderCertificate(sender *SealedSenderAddress, publicKey *PublicKey, expiration time.Time, signerCertificate *ServerCertificate, signerKey *PrivateKey) (*SenderCertificate, error) { func NewSenderCertificate(sender *SealedSenderAddress, publicKey *PublicKey, expiration time.Time, signerCertificate *ServerCertificate, signerKey *PrivateKey) (*SenderCertificate, error) {
var sc C.SignalMutPointerSenderCertificate 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( signalFfiError := C.signal_sender_certificate_new(
&sc, &sc,
senderUUIDStr, C.CString(sender.UUID.String()),
senderE164Str, C.CString(sender.E164),
C.uint32_t(sender.DeviceID), C.uint32_t(sender.DeviceID),
publicKey.constPtr(), publicKey.constPtr(),
C.uint64_t(expiration.UnixMilli()), C.uint64_t(expiration.UnixMilli()),
@ -139,7 +135,7 @@ func (sc *SenderCertificate) GetSignature() ([]byte, error) {
} }
func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, 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()) signalFfiError := C.signal_sender_certificate_get_sender_uuid(&rawUUID, sc.constPtr())
runtime.KeepAlive(sc) runtime.KeepAlive(sc)
if signalFfiError != nil { if signalFfiError != nil {
@ -149,7 +145,7 @@ func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, error) {
} }
func (sc *SenderCertificate) GetSenderE164() (string, 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()) signalFfiError := C.signal_sender_certificate_get_sender_e164(&e164, sc.constPtr())
runtime.KeepAlive(sc) runtime.KeepAlive(sc)
if signalFfiError != nil { 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( signalFfiError := C.signal_sender_key_distribution_message_create(
&skdm, &skdm,
sender.constPtr(), sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)), (*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
callbackCtx.wrapSenderKeyStore(store), callbackCtx.wrapSenderKeyStore(store),
) )
runtime.KeepAlive(sender) runtime.KeepAlive(sender)

View file

@ -20,9 +20,13 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_sender_key_callback(void *store_ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id); typedef const SignalProtocolAddress const_address;
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 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 "C"
import ( import (
@ -38,40 +42,36 @@ type SenderKeyStore interface {
} }
//export signal_load_sender_key_callback //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 { 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 { if err == nil && record != nil {
record.CancelFinalizer() record.CancelFinalizer()
recordp.raw = record.ptr *recordp = record.ptr
} }
return err return err
}) })
} }
//export signal_store_sender_key_callback //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 { 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() cloned, err := record.Clone()
if err != nil { if err != nil {
return err 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 { func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct {
return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{ return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_sender_key: C.SignalFfiSenderKeyStoreLoadSenderKey(C.signal_load_sender_key_callback), load_sender_key: C.SignalLoadSenderKey(C.signal_load_sender_key_callback),
store_sender_key: C.SignalFfiSenderKeyStoreStoreSenderKey(C.signal_store_sender_key_callback), store_sender_key: C.SignalStoreSenderKey(C.signal_store_sender_key_callback),
destroy: C.SignalFfiSenderKeyStoreDestroy(C.signal_destroy_sender_key_store_callback),
}} }}
} }

View file

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

View file

@ -31,6 +31,12 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
func init() {
if C.SignalUUID_LEN != 16 {
panic("libsignal-ffi uuid type size mismatch")
}
}
type ServiceIDType byte type ServiceIDType byte
const ( const (
@ -87,9 +93,6 @@ func (s ServiceID) IsEmpty() bool {
} }
func (s ServiceID) Address(deviceID uint) (*Address, error) { 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) 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) 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) { func (s ServiceID) MarshalZerologObject(e *zerolog.Event) {
e.Stringer("type", s.Type) e.Stringer("type", s.Type)
e.Stringer("uuid", s.UUID) e.Stringer("uuid", s.UUID)
} }
const ServiceIDUUIDLength = 16 type ServiceIDFixedBytes [17]byte
const ServiceIDFixedBytesLength = 17
type ServiceIDFixedBytes = fixedArray17
func (s ServiceID) FixedBytes() *ServiceIDFixedBytes { func (s ServiceID) FixedBytes() *ServiceIDFixedBytes {
var result ServiceIDFixedBytes var result ServiceIDFixedBytes
@ -164,19 +151,7 @@ func ServiceIDFromString(val string) (ServiceID, error) {
} }
} }
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) { func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
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 {
var id ServiceID var id ServiceID
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID)) fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))
id.Type = ServiceIDType(fixedBytes[0]) id.Type = ServiceIDType(fixedBytes[0])
@ -184,10 +159,6 @@ func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) Serv
return id return id
} }
func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t { func (s ServiceID) CFixedBytes() cPNIType {
return s.FixedBytes().cFixedArray() return cPNIType(unsafe.Pointer(s.FixedBytes()))
}
func (s ServiceID) cConstFixedArray() cFixedArray17Compat {
return cFixedArray17Compat(s.cFixedArray())
} }

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

View file

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

View file

@ -20,9 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_session_callback(void *store_ctx, SignalMutPointerSessionRecord *recordp, SignalMutPointerProtocolAddress address); typedef const SignalSessionRecord const_session_record;
extern int signal_store_session_callback(void *store_ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record); typedef const SignalProtocolAddress const_address;
extern void signal_destroy_session_store_callback(void *store_ctx);
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 "C"
import ( import (
@ -36,39 +38,33 @@ type SessionStore interface {
} }
//export signal_load_session_callback //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 { 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 { if err == nil && record != nil {
record.CancelFinalizer() record.CancelFinalizer()
recordp.raw = record.ptr *recordp = record.ptr
} }
return err return err
}) })
} }
//export signal_store_session_callback //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 { 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() cloned, err := record.Clone()
if err != nil { if err != nil {
return err 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 { func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct {
return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{ return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_session: C.SignalFfiSessionStoreLoadSession(C.signal_load_session_callback), load_session: C.SignalLoadSession(C.signal_load_session_callback),
store_session: C.SignalFfiSessionStoreStoreSession(C.signal_store_session_callback), store_session: C.SignalStoreSession(C.signal_store_session_callback),
destroy: C.SignalFfiSessionStoreDestroy(C.signal_destroy_session_store_callback),
}} }}
} }

View file

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

View file

@ -20,9 +20,10 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalMutPointerSignedPreKeyRecord *recordp, uint32_t id); typedef const SignalSignedPreKeyRecord const_signed_pre_key_record;
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); 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 "C"
import ( import (
@ -36,21 +37,21 @@ type SignedPreKeyStore interface {
} }
//export signal_load_signed_pre_key_callback //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 { return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
key, err := store.LoadSignedPreKey(ctx, uint32(id)) key, err := store.LoadSignedPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_signed_pre_key_callback //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 { 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() cloned, err := record.Clone()
if err != nil { if err != nil {
return err 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 { func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct {
return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{ return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_signed_pre_key: C.SignalFfiSignedPreKeyStoreLoadSignedPreKey(C.signal_load_signed_pre_key_callback), load_signed_pre_key: C.SignalLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
store_signed_pre_key: C.SignalFfiSignedPreKeyStoreStoreSignedPreKey(C.signal_store_signed_pre_key_callback), store_signed_pre_key: C.SignalStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
destroy: C.SignalFfiSignedPreKeyStoreDestroy(C.signal_destroy_signed_pre_key_store_callback),
}} }}
} }

View file

@ -1,10 +1,11 @@
#!/bin/sh #!/bin/sh
cd /data cd /data
export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" 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 cd libsignal
cargo build -p libsignal-ffi --release cargo build -p libsignal-ffi --release
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
cd .. cd ..
mv libsignal/target/release/libsignal_ffi.a . 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 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 # Build libsignal
cargo build -p libsignal-ffi --release 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 # Navigate back to the original directory
cd "$ORIGINAL_DIR" cd "$ORIGINAL_DIR"
# Copy files from the libsignal directory # Copy files from the libsignal directory
cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" . 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." echo "Files copied successfully."

View file

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

View file

@ -44,6 +44,7 @@ func (mc *MessageConverter) ToSignal(
portal *bridgev2.Portal, portal *bridgev2.Portal,
evt *event.Event, evt *event.Event,
content *event.MessageEventContent, content *event.MessageEventContent,
timestamp uint64,
relaybotFormatted bool, relaybotFormatted bool,
replyTo *database.Message, replyTo *database.Message,
) (*signalpb.DataMessage, error) { ) (*signalpb.DataMessage, error) {
@ -54,15 +55,16 @@ func (mc *MessageConverter) ToSignal(
} }
dm := &signalpb.DataMessage{ dm := &signalpb.DataMessage{
Preview: mc.convertURLPreviewToSignal(ctx, content), Timestamp: &timestamp,
Preview: mc.convertURLPreviewToSignal(ctx, content),
} }
if replyTo != nil { if replyTo != nil {
authorACI, messageID, err := signalid.ParseMessageID(replyTo.ID) authorACI, messageID, err := signalid.ParseMessageID(replyTo.ID)
if err == nil { if err == nil {
dm.Quote = &signalpb.DataMessage_Quote{ dm.Quote = &signalpb.DataMessage_Quote{
Id: proto.Uint64(messageID), Id: proto.Uint64(messageID),
AuthorAciBinary: authorACI[:], AuthorAci: proto.String(authorACI.String()),
Type: signalpb.DataMessage_Quote_NORMAL.Enum(), Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
} }
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments { if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
dm.Quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1) dm.Quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1)
@ -110,24 +112,21 @@ func (mc *MessageConverter) ToSignal(
return nil, fmt.Errorf("failed to convert sticker: %w", err) return nil, fmt.Errorf("failed to convert sticker: %w", err)
} }
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS)) att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
var emoji *string
dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker) // TODO check for single grapheme cluster?
if dm.Sticker == nil { if len([]rune(content.Body)) == 1 {
var emoji *string emoji = proto.String(variationselector.Remove(content.Body))
// TODO check for single grapheme cluster? }
if len([]rune(content.Body)) == 1 { dm.Sticker = &signalpb.DataMessage_Sticker{
emoji = proto.String(variationselector.Remove(content.Body)) // Signal iOS validates that pack id/key are of the correct length.
} // Android is fine with any non-nil values (like a zero-length byte string).
dm.Sticker = &signalpb.DataMessage_Sticker{ PackId: make([]byte, 16),
// Signal iOS validates that pack id/key are of the correct length. PackKey: make([]byte, 32),
// Android is fine with any non-nil values (like a zero-length byte string). StickerId: proto.Uint32(0),
PackId: make([]byte, 16),
PackKey: make([]byte, 32), Data: att,
StickerId: proto.Uint32(0), Emoji: emoji,
Emoji: emoji,
}
} }
dm.Sticker.Data = att
case event.MsgLocation: case event.MsgLocation:
lat, lon, err := parseGeoURI(content.GeoURI) lat, lon, err := parseGeoURI(content.GeoURI)
if err != nil { if err != nil {

View file

@ -81,16 +81,6 @@ func BackupToDataMessage(ci *backuppb.ChatItem, attMap AttachmentMap) (*signalpb
Emoji: ti.StickerMessage.Sticker.Emoji, Emoji: ti.StickerMessage.Sticker.Emoji,
Data: backupToSignalAttachment(ti.StickerMessage.Sticker.Data, 0, uuid.New(), attMap), 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: case *backuppb.ChatItem_RemoteDeletedMessage:
// TODO handle some other way? (also disappeared view-once messages) // TODO handle some other way? (also disappeared view-once messages)
return nil, nil return nil, nil
@ -248,7 +238,11 @@ func backupToSignalBodyRange(from *backuppb.BodyRange) *signalpb.BodyRange {
out.Length = &from.Length out.Length = &from.Length
switch av := from.AssociatedValue.(type) { switch av := from.AssociatedValue.(type) {
case *backuppb.BodyRange_MentionAci: 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_: case *backuppb.BodyRange_Style_:
out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)} out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)}
} }

View file

@ -22,11 +22,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"mime"
"net/http" "net/http"
"os"
"strconv"
"strings" "strings"
"time" "time"
@ -55,7 +51,7 @@ func calculateLength(dm *signalpb.DataMessage) int {
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 { if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
return 1 return 1
} }
if dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil { if dm.Sticker != nil {
return 1 return 1
} }
length := len(dm.Attachments) + len(dm.Contact) length := len(dm.Attachments) + len(dm.Contact)
@ -79,13 +75,11 @@ func CanConvertSignal(dm *signalpb.DataMessage) bool {
} }
const ViewOnceDisappearTimer = 5 * time.Minute 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( func (mc *MessageConverter) ToMatrix(
ctx context.Context, ctx context.Context,
client *signalmeow.Client, client *signalmeow.Client,
portal *bridgev2.Portal, portal *bridgev2.Portal,
sender uuid.UUID,
intent bridgev2.MatrixAPI, intent bridgev2.MatrixAPI,
dm *signalpb.DataMessage, dm *signalpb.DataMessage,
attMap AttachmentMap, attMap AttachmentMap,
@ -114,20 +108,8 @@ func (mc *MessageConverter) ToMatrix(
// Don't allow any other parts in a sticker message // Don't allow any other parts in a sticker message
return cm 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() { 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)) cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att, attMap))
} else { } else {
longBody, err := mc.downloadSignalLongText(ctx, att, attMap) longBody, err := mc.downloadSignalLongText(ctx, att, attMap)
@ -178,12 +160,9 @@ func (mc *MessageConverter) ToMatrix(
} }
} }
if dm.Quote != nil { if dm.Quote != nil {
authorACI, err := signalmeow.ParseStringOrBinaryUUID(dm.Quote.GetAuthorAci(), dm.Quote.GetAuthorAciBinary()) authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err). zerolog.Ctx(ctx).Err(err).Str("author_aci", dm.Quote.GetAuthorAci()).Msg("Failed to parse quote author ACI")
Str("author_aci", dm.Quote.GetAuthorAci()).
Hex("author_aci_binary", dm.Quote.GetAuthorAciBinary()).
Msg("Failed to parse quote author ACI")
} else { } else {
cm.ReplyTo = &networkid.MessageOptionalPartID{ cm.ReplyTo = &networkid.MessageOptionalPartID{
MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()), MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()),
@ -344,7 +323,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
card.Add(vcard.FieldTelephone, &field) card.Add(vcard.FieldTelephone, &field)
} }
if contact.GetAvatar().GetAvatar() != nil { 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 { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar") zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar")
} else { } 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 // Signal stickers are 512x512, so tell Matrix clients to render them as 256x256
// https://github.com/signalapp/Signal-Desktop/blob/v7.77.0-beta.1/ts/components/conversation/Message.dom.tsx#L135
if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 { if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 {
converted.Content.Info.Width = 200 converted.Content.Info.Width = 256
converted.Content.Info.Height = 200 converted.Content.Info.Height = 256
} }
converted.Content.Body = sticker.GetEmoji() 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.Type = event.EventSticker
converted.Content.MsgType = "" 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 return converted
} }
func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -509,9 +491,7 @@ func checkIfAttachmentExists(att *signalpb.AttachmentPointer, attMap AttachmentM
return nil return nil
} }
func (mc *MessageConverter) downloadAttachment( func (mc *MessageConverter) downloadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) ([]byte, error) {
ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap, into *os.File,
) ([]byte, error) {
if err := checkIfAttachmentExists(att, attMap); err != nil { if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err return nil, err
} }
@ -522,19 +502,19 @@ func (mc *MessageConverter) downloadAttachment(
plaintextHash = target.GetPlaintextHash() 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) { func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*bridgev2.ConvertedMessagePart, error) {
fileName := att.GetFileName()
content := &event.MessageEventContent{ content := &event.MessageEventContent{
Body: att.GetFileName(),
Info: &event.FileInfo{ Info: &event.FileInfo{
MimeType: att.GetContentType(), Width: int(att.GetWidth()),
Width: int(att.GetWidth()), Height: int(att.GetHeight()),
Height: int(att.GetHeight()), Size: int(att.GetSize()),
Size: int(att.GetSize()),
}, },
} }
mimeType := att.GetContentType()
if err := checkIfAttachmentExists(att, attMap); err != nil { if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err return nil, err
} else if mc.DirectMedia { } 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) content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
} else { } 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 { if err != nil {
return nil, err return nil, err
} }
@ -570,20 +568,15 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
content.Info.Blurhash = att.GetBlurHash() content.Info.Blurhash = att.GetBlurHash()
content.Info.AnoaBlurhash = att.GetBlurHash() content.Info.AnoaBlurhash = att.GetBlurHash()
} }
plainMime, _, _ := mime.ParseMediaType(content.Info.MimeType) switch strings.Split(mimeType, "/")[0] {
// Supported mime types from https://github.com/signalapp/Signal-Desktop/blob/main/ts/util/GoogleChrome.std.ts case "image":
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":
content.MsgType = event.MsgImage content.MsgType = event.MsgImage
case "video/mp4", "video/ogg", "video/webm": case "video":
content.MsgType = event.MsgVideo content.MsgType = event.MsgVideo
case "audio":
content.MsgType = event.MsgAudio
default: default:
if strings.HasPrefix(plainMime, "audio/") && !strings.HasSuffix(plainMime, "aiff") { content.MsgType = event.MsgFile
content.MsgType = event.MsgAudio
} else {
content.MsgType = event.MsgFile
}
} }
var extra map[string]any var extra map[string]any
if att.GetFlags()&uint32(signalpb.AttachmentPointer_GIF) != 0 { if att.GetFlags()&uint32(signalpb.AttachmentPointer_GIF) != 0 {
@ -597,8 +590,10 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
}, },
} }
} }
content.Body = fileName
content.Info.MimeType = mimeType
if content.Body == "" { 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{ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage, Type: event.EventMessage,
@ -606,203 +601,3 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
Extra: extra, Extra: extra,
}, nil }, 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", 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>", 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{{ ent: signalfmt.BodyRangeList{{
Start: 9, Start: 9,
Length: 4, Length: 4,
Value: signalfmt.StyleBold, 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, Length: 6,
Value: signalfmt.StyleBold, Value: signalfmt.StyleBold,
}}, }},
@ -125,13 +134,21 @@ func TestParse_HTML(t *testing.T) {
{ {
name: "OrderedList", 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>", 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{{ ent: signalfmt.BodyRangeList{{
Start: 13, Start: 13,
Length: 4, Length: 4,
Value: signalfmt.StyleBold, Value: signalfmt.StyleBold,
}, { }, {
Start: 75, Start: 22,
Length: 3,
Value: signalfmt.StyleMonospace,
}, {
Start: 30,
Length: 4,
Value: signalfmt.StyleMonospace,
}, {
Start: 59,
Length: 6, Length: 6,
Value: signalfmt.StyleBold, Value: signalfmt.StyleBold,
}}, }},

View file

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

View file

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

View file

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

View file

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

View file

@ -28,14 +28,11 @@ type PortalMetadata struct {
} }
type MessageMetadata struct { type MessageMetadata struct {
ContainsAttachments bool `json:"contains_attachments,omitempty"` 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 { type UserLoginMetadata struct {
ChatsSynced bool `json:"chats_synced,omitempty"` ChatsSynced bool `json:"chats_synced,omitempty"`
LastContactSync jsontime.UnixMilli `json:"last_contact_sync,omitempty"`
} }
type GhostMetadata struct { type GhostMetadata struct {

View file

@ -48,33 +48,19 @@ func ParseUserLoginID(userLoginID networkid.UserLoginID) (uuid.UUID, error) {
return userID, nil return userID, nil
} }
func toServiceID(id uuid.UUID, err error) (libsignalgo.ServiceID, error) { func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (uuid.UUID, error) {
if err != nil {
return libsignalgo.ServiceID{}, err
}
return libsignalgo.NewACIServiceID(id), nil
}
func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (libsignalgo.ServiceID, error) {
switch ghostOrUserLogin := ghostOrUserLogin.(type) { switch ghostOrUserLogin := ghostOrUserLogin.(type) {
case *bridgev2.UserLogin: case *bridgev2.UserLogin:
return toServiceID(ParseUserLoginID(ghostOrUserLogin.ID)) return ParseUserLoginID(ghostOrUserLogin.ID)
case *bridgev2.Ghost: case *bridgev2.Ghost:
return ParseUserIDAsServiceID(ghostOrUserLogin.ID) return ParseUserID(ghostOrUserLogin.ID)
default: default:
return libsignalgo.ServiceID{}, fmt.Errorf("cannot parse ID: unknown type: %T", ghostOrUserLogin) return uuid.Nil, fmt.Errorf("cannot parse ID: unknown type: %T", ghostOrUserLogin)
} }
} }
const pniUserIDPrefix = "pni_"
const pniServiceIDPrefix = "PNI:"
func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) { func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) {
userIDStr := string(userID) return libsignalgo.ServiceIDFromString(string(userID))
if strings.HasPrefix(userIDStr, pniUserIDPrefix) {
userIDStr = pniServiceIDPrefix + userIDStr[len(pniUserIDPrefix):]
}
return libsignalgo.ServiceIDFromString(userIDStr)
} }
func ParsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) { func ParsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) {
@ -117,14 +103,7 @@ func MakeUserID(user uuid.UUID) networkid.UserID {
} }
func MakeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID { func MakeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID {
switch user.Type { return networkid.UserID(user.String())
case libsignalgo.ServiceIDTypeACI:
return MakeUserID(user.UUID)
case libsignalgo.ServiceIDTypePNI:
return networkid.UserID(pniUserIDPrefix + user.UUID.String())
default:
panic(fmt.Errorf("invalid service ID type %d", user.Type))
}
} }
func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID { func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID {

View file

@ -34,7 +34,6 @@ const (
directMediaTypeGroupAvatar directMediaType = 1 directMediaTypeGroupAvatar directMediaType = 1
directMediaTypeProfileAvatar directMediaType = 2 directMediaTypeProfileAvatar directMediaType = 2
directMediaTypePlaintextDigestAttachment directMediaType = 3 directMediaTypePlaintextDigestAttachment directMediaType = 3
directMediaTypeSticker directMediaType = 4
) )
type DirectMediaInfo interface { type DirectMediaInfo interface {
@ -45,7 +44,6 @@ var (
_ DirectMediaInfo = (*DirectMediaAttachment)(nil) _ DirectMediaInfo = (*DirectMediaAttachment)(nil)
_ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil) _ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil) _ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaSticker)(nil)
) )
type DirectMediaAttachment struct { type DirectMediaAttachment struct {
@ -129,30 +127,6 @@ func (m DirectMediaProfileAvatar) AsMediaID() (mediaID networkid.MediaID, err er
return networkid.MediaID(buf.Bytes()), nil 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) { func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err error) {
mediaIDLen := len(mediaID) mediaIDLen := len(mediaID)
if mediaIDLen == 0 { if mediaIDLen == 0 {
@ -226,15 +200,6 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err err
info.ProfileAvatarPath = string(profileAvatarPath) info.ProfileAvatarPath = string(profileAvatarPath)
} }
return &info, nil 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) return nil, fmt.Errorf("invalid direct media type %d", mediaType)

View file

@ -31,11 +31,8 @@ import (
"math" "math"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/fallocate"
"go.mau.fi/util/pkcs7"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"google.golang.org/protobuf/proto" "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 ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment")
var ErrAttachmentNotFound = errors.New("attachment not found on server") 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() digest := a.GetDigest()
plaintextDigest := false plaintextDigest := false
if digest == nil && plaintextHash != nil { if digest == nil && plaintextHash != nil {
digest = plaintextHash digest = plaintextHash
plaintextDigest = true plaintextDigest = true
} }
return DownloadAttachment( return DownloadAttachment(ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize())
ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize(), into,
)
} }
func DownloadAttachment( func DownloadAttachment(ctx context.Context, cdnID uint64, cdnKey string, cdnNumber uint32, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
ctx context.Context, path := getAttachmentPath(cdnID, cdnKey)
cdnID uint64, resp, err := web.GetAttachment(ctx, path, cdnNumber, nil)
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { bodyReader := resp.Body
_ = resp.Body.Close() defer bodyReader.Close()
}()
var body []byte body, err := io.ReadAll(bodyReader)
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)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,27 +94,12 @@ func DownloadAttachment(
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) 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) return decryptAttachment(body, key, digest, plaintextDigest, size)
} }
const MACLength = 32 const MACLength = 32
const IVLength = 16 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) { func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
if !plaintextDigest { if !plaintextDigest {
hash := sha256.Sum256(body) hash := sha256.Sum256(body)
@ -153,7 +107,12 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return nil, ErrInvalidDigestForAttachment 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 { if err != nil {
return nil, err return nil, err
} }
@ -170,59 +129,6 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return decrypted, nil 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 { type attachmentV4UploadAttributes struct {
Cdn uint32 `json:"cdn"` Cdn uint32 `json:"cdn"`
Key string `json:"key"` 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) { func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger() log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
keys := random.Bytes(64) // combined AES and MAC keys 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) body = extend(body, paddedLen)
encryptedWithMAC, err := macAndAESEncrypt(keys, body) encrypted, err := aesEncrypt(keys[:32], body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
encryptedWithMAC := appendMAC(keys[32:], encrypted)
// Get upload attributes from Signal server // Get upload attributes from Signal server
attributesPath := "/v4/attachments/form/upload" 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 { if err != nil {
log.Err(err).Msg("Failed to request upload attributes") log.Err(err).Msg("Failed to request upload attributes")
return nil, fmt.Errorf("failed to request upload attributes: %w", err) return nil, fmt.Errorf("failed to request upload attributes: %w", err)
} }
var uploadAttributes attachmentV4UploadAttributes var uploadAttributes attachmentV4UploadAttributes
err = web.DecodeWSResponseBody(ctx, &uploadAttributes, resp) err = web.DecodeHTTPResponseBody(ctx, &uploadAttributes, resp)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to decode upload attributes") log.Err(err).Msg("Failed to decode upload attributes")
return nil, fmt.Errorf("failed to decode upload attributes: %w", err) 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) err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC)
} else { } else {
log.Trace().Msg("Using legacy upload") log.Trace().Msg("Using legacy upload")
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC) err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC, username, password)
} }
if err != nil { if err != nil {
log.Err(err).Msg("Failed to upload attachment") log.Err(err).Msg("Failed to upload attachment")
@ -317,17 +218,17 @@ func (cli *Client) uploadAttachmentLegacy(
ctx context.Context, ctx context.Context,
uploadAttributes attachmentV4UploadAttributes, uploadAttributes attachmentV4UploadAttributes,
encryptedWithMAC []byte, encryptedWithMAC []byte,
username string,
password string,
) error { ) error {
username, password := cli.Store.BasicAuthCreds()
// Allocate attachment on CDN // 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, OverrideURL: uploadAttributes.SignedUploadLocation,
ContentType: web.ContentTypeOctetStream, ContentType: web.ContentTypeOctetStream,
Headers: uploadAttributes.Headers, Headers: uploadAttributes.Headers,
Username: &username, Username: &username,
Password: &password, Password: &password,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
return fmt.Errorf("failed to send allocate request: %w", err) return fmt.Errorf("failed to send allocate request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 { } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -335,14 +236,13 @@ func (cli *Client) uploadAttachmentLegacy(
} }
// Upload attachment to CDN // 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"), OverrideURL: resp.Header.Get("Location"),
Body: encryptedWithMAC, Body: encryptedWithMAC,
ContentType: web.ContentTypeOctetStream, ContentType: web.ContentTypeOctetStream,
Username: &username, Username: &username,
Password: &password, Password: &password,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
return fmt.Errorf("failed to send upload request: %w", err) return fmt.Errorf("failed to send upload request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 { } 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-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC))
uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key)) 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, OverrideURL: uploadAttributes.SignedUploadLocation,
Body: encryptedWithMAC, Body: encryptedWithMAC,
ContentType: web.ContentTypeOffsetOctetStream, ContentType: web.ContentTypeOffsetOctetStream,
Headers: uploadAttributes.Headers, Headers: uploadAttributes.Headers,
}) })
web.CloseBody(resp)
// TODO actually support resuming on error // TODO actually support resuming on error
if err != nil { if err != nil {
return fmt.Errorf("failed to send upload request: %w", err) return fmt.Errorf("failed to send upload request: %w", err)
@ -381,17 +280,12 @@ func (cli *Client) uploadAttachmentTUS(
return nil 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) log := zerolog.Ctx(ctx)
if groupMasterKey == "" { groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
var err error if err != nil {
groupMasterKey, err = cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid) log.Err(err).Msg("Could not get master key from group id")
if err != nil { return "", err
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)) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil { if err != nil {
@ -411,15 +305,14 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
} }
// Get upload form from Signal server // Get upload form from Signal server
formPath := "/v2/groups/avatar/form" formPath := "/v1/groups/avatar/form"
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf} opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf, Host: web.StorageHostname}
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, formPath, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodGet, formPath, opts)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request fetching avatar upload form") log.Err(err).Msg("Error sending request fetching avatar upload form")
return "", err return "", err
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
web.CloseBody(resp)
if err != nil { if err != nil {
log.Err(err).Msg("Error decoding response body fetching upload attributes") log.Err(err).Msg("Error decoding response body fetching upload attributes")
return "", err return "", err
@ -445,11 +338,11 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
w.Close() w.Close()
// Upload avatar to CDN // 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(), Body: requestBody.Bytes(),
ContentType: web.ContentType(w.FormDataContentType()), ContentType: web.ContentType(w.FormDataContentType()),
Host: web.CDN1Hostname,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request uploading attachment") log.Err(err).Msg("Error sending request uploading attachment")
return "", err 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) return nil, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", len(ciphertext)%aes.BlockSize)
} }
iv := ciphertext[:IVLength] iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[IVLength:]
mode := cipher.NewCBCDecrypter(block, iv) mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext) mode.CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext) pad := ciphertext[len(ciphertext)-1]
}
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
}
}
if pad > aes.BlockSize { 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 { func appendMAC(key, body []byte) []byte {
@ -542,11 +393,14 @@ func aesEncrypt(key, plaintext []byte) ([]byte, error) {
return nil, err 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) iv := random.Bytes(16)
mode := cipher.NewCBCEncrypter(block, iv) 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 { 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 { if err != nil {
return fmt.Errorf("failed to download transfer archive: %w", err) 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) reqDuration := time.Since(reqStart)
if reqDuration < reqTimeout-10*time.Second { if reqDuration < reqTimeout-10*time.Second {
select { time.Sleep(15 * time.Second)
case <-time.After(15 * time.Second):
case <-ctx.Done():
return nil, ctx.Err()
}
} }
} }
} }
@ -295,14 +291,21 @@ func (cli *Client) tryRequestTransferArchive(ctx context.Context, timeout time.D
reqCtx, cancel := context.WithTimeout(ctx, timeout+15*time.Second) reqCtx, cancel := context.WithTimeout(ctx, timeout+15*time.Second)
defer cancel() defer cancel()
path := "/v1/devices/transfer_archive?timeout=" + strconv.Itoa(int(timeout.Seconds())) 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 { if err != nil {
return nil, err return nil, err
} else if resp.GetStatus() == http.StatusNoContent { } else if resp.StatusCode == http.StatusNoContent {
return nil, nil return nil, nil
} else if resp.GetStatus() != http.StatusOK { } else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.GetStatus()) return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
} else if err = json.Unmarshal(resp.Body, &respBody); err != nil { } else if err = json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
} else { } else {
return respBody, nil return respBody, nil

View file

@ -18,21 +18,16 @@ package signalmeow
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events" "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/store"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web" "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
@ -40,16 +35,14 @@ type Client struct {
Store *store.Device Store *store.Device
Log zerolog.Logger Log zerolog.Logger
senderCertificateWithE164 *libsignalgo.SenderCertificate SenderCertificateWithE164 *libsignalgo.SenderCertificate
senderCertificateNoE164 *libsignalgo.SenderCertificate SenderCertificateNoE164 *libsignalgo.SenderCertificate
senderCertificateCache sync.Mutex GroupCredentials *GroupCredentials
GroupCache *GroupCache
sendCache *exsync.RingBuffer[sendCacheKey, *signalpb.Content] ProfileCache *ProfileCache
GroupCallCache *map[string]bool
GroupCache *GroupCache LastContactRequestTime time.Time
ProfileCache *ProfileCache SyncContactsOnConnect bool
LastContactRequestTime time.Time
SyncContactsOnConnect bool
encryptionLock sync.Mutex encryptionLock sync.Mutex
@ -71,26 +64,6 @@ type Client struct {
writeCallbackCounter chan time.Time 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 { func (cli *Client) handleEvent(evt events.SignalEvent) bool {
return cli.EventHandler(evt) return cli.EventHandler(evt)
} }
@ -137,11 +110,3 @@ func (cli *Client) connectUnauthedWS(ctx context.Context) (chan web.SignalWebsoc
func (cli *Client) IsLoggedIn() bool { func (cli *Client) IsLoggedIn() bool {
return cli.Store != nil && cli.Store.IsDeviceLoggedIn() 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -35,14 +35,12 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "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" "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 ProdContactDiscoveryServer = "cdsi.signal.org"
const ProdContactDiscoveryMrenclave = "ee9503070127120074612b6688e593b67e486b1541449f54d71e387484eb40a3"
const ContactDiscoveryAuthTTL = 23 * time.Hour const ContactDiscoveryAuthTTL = 23 * time.Hour
const rateLimitCloseCode = websocket.StatusCode(4008) 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) ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel() 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 // TODO figure out if tokens are useful
// (it's meant for old_e164s) // (it's meant for old_e164s)
//Token: cli.cdToken, //Token: cli.cdToken,
@ -95,7 +93,7 @@ func (cli *Client) LookupPhone(ctx context.Context, e164s ...uint64) (ContactDis
return resp, err 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) creds, err := cli.getContactDiscoveryCredentials(ctx)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to fetch contact discovery auth: %w", err) 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 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) plaintext, err := proto.Marshal(req)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal request: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to decrypt message: %w", err) return fmt.Errorf("failed to decrypt message: %w", err)
} }
var cdsClientResp cds2pb.ClientResponse var cdsClientResp signalpb.CDSClientResponse
err = proto.Unmarshal(decrypted, &cdsClientResp) err = proto.Unmarshal(decrypted, &cdsClientResp)
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal message: %w", err) return fmt.Errorf("failed to unmarshal message: %w", err)
} }
if cdsClientResp.Token != nil { if cdsClientResp.Token != nil {
cdc.Token = cdsClientResp.Token cdc.Token = cdsClientResp.Token
err = cdc.SendRequest(ctx, &cds2pb.ClientRequest{ err = cdc.SendRequest(ctx, &signalpb.CDSClientRequest{
TokenAck: true, TokenAck: proto.Bool(true),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to send token ack request: %w", err) 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" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
func hmacSHA256(key, input []byte) []byte { func hmacSHA256(key, input []byte) []byte {
@ -62,12 +63,18 @@ func (cli *Client) updateDeviceName(ctx context.Context, encryptedName []byte) e
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal device name update request: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to send device name update request: %w", err) return fmt.Errorf("failed to send device name update request: %w", err)
} }
if resp.GetStatus() < 200 || resp.GetStatus() >= 300 { defer resp.Body.Close()
return fmt.Errorf("device name update request returned status %d", resp.GetStatus()) if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("device name update request returned status %d", resp.StatusCode)
} }
return nil return nil
} }

View file

@ -28,17 +28,16 @@ type SignalEvent interface {
isSignalEvent() isSignalEvent()
} }
func (*ChatEvent) isSignalEvent() {} func (*ChatEvent) isSignalEvent() {}
func (*DecryptionError) isSignalEvent() {} func (*DecryptionError) isSignalEvent() {}
func (*Receipt) isSignalEvent() {} func (*Receipt) isSignalEvent() {}
func (*ReadSelf) isSignalEvent() {} func (*ReadSelf) isSignalEvent() {}
func (*Call) isSignalEvent() {} func (*Call) isSignalEvent() {}
func (*ContactList) isSignalEvent() {} func (*ContactList) isSignalEvent() {}
func (*ACIFound) isSignalEvent() {} func (*ACIFound) isSignalEvent() {}
func (*DeleteForMe) isSignalEvent() {} func (*DeleteForMe) isSignalEvent() {}
func (*MessageRequestResponse) isSignalEvent() {} func (*QueueEmpty) isSignalEvent() {}
func (*QueueEmpty) isSignalEvent() {} func (*LoggedOut) isSignalEvent() {}
func (*LoggedOut) isSignalEvent() {}
type MessageInfo struct { type MessageInfo struct {
Sender uuid.UUID Sender uuid.UUID
@ -90,14 +89,6 @@ type DeleteForMe struct {
*signalpb.SyncMessage_DeleteForMe *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 QueueEmpty struct{}
type LoggedOut struct{ Error error } 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/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -91,12 +90,6 @@ type Group struct {
//PublicKey *libsignalgo.PublicKey //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) { func (group *Group) GetInviteLink() (string, error) {
if group.InviteLinkPassword == nil { if group.InviteLinkPassword == nil {
return "", fmt.Errorf("no invite link password set") return "", fmt.Errorf("no invite link password set")
@ -106,8 +99,8 @@ func (group *Group) GetInviteLink() (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("couldn't decode invite link password") return "", fmt.Errorf("couldn't decode invite link password")
} }
inviteLinkContents := signalpb.GroupInviteLink_ContentsV1{ inviteLinkContents := signalpb.GroupInviteLink_V1Contents{
ContentsV1: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{ V1Contents: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
GroupMasterKey: masterKeyBytes[:], GroupMasterKey: masterKeyBytes[:],
InviteLinkPassword: inviteLinkPasswordBytes, InviteLinkPassword: inviteLinkPasswordBytes,
}, },
@ -121,15 +114,6 @@ func (group *Group) GetInviteLink() (string, error) {
return "https://signal.group/#" + inviteLinkPath, nil 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 { type GroupAccessControl struct {
Members AccessControl Members AccessControl
AddFromInviteLink AccessControl AddFromInviteLink AccessControl
@ -226,7 +210,8 @@ func (groupChange *GroupChange) isEmpty() bool {
len(groupChange.PromoteRequestingMembers) == 0 && len(groupChange.PromoteRequestingMembers) == 0 &&
groupChange.ModifyDescription == nil && groupChange.ModifyDescription == nil &&
groupChange.ModifyAnnouncementsOnly == nil && groupChange.ModifyAnnouncementsOnly == nil &&
len(groupChange.AddBannedMembers) == 0 len(groupChange.AddBannedMembers) == 0 &&
len(groupChange.DeleteMembers) == 0
} }
func (groupChange *GroupChange) resolveConflict(group *Group) { func (groupChange *GroupChange) resolveConflict(group *Group) {
@ -329,7 +314,8 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
Logger() Logger()
sevenDaysOut := today.Add(7 * 24 * time.Hour) sevenDaysOut := today.Add(7 * 24 * time.Hour)
path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix()) 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 { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) 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") log.Err(err).Msg("json.Unmarshal error")
return nil, err return nil, err
} }
// make sure pni matches device pni
if creds.PNI != cli.Store.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 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) { func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) {
log := zerolog.Ctx(ctx).With(). log := zerolog.Ctx(ctx).With().
Str("action", "get authorization for today"). Str("action", "get authorization for today").
Logger() 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 err != nil { if todayCred == nil {
return nil, fmt.Errorf("failed to get group credentials: %w", err) creds, err := cli.fetchNewGroupCreds(ctx, today)
if err != nil {
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) redemptionTime := uint64(todayCred.RedemptionTime)
credential := todayCred.Credential credential := todayCred.Credential
authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(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) descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description)
if err == nil { if err == nil {
// treat a failure in obtaining the description as non-fatal // 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 { 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 // These aren't encrypted
decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.AvatarPath = encryptedGroup.Avatar
decryptedGroup.Revision = encryptedGroup.Version decryptedGroup.Revision = encryptedGroup.Revision
// Decrypt members // Decrypt members
for _, member := range encryptedGroup.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) decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember)
} }
for _, pendingMember := range encryptedGroup.MembersPendingProfileKey { for _, pendingMember := range encryptedGroup.PendingMembers {
if pendingMember == nil { if pendingMember == nil {
continue continue
} }
@ -509,7 +524,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember) decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember)
} }
for _, requestingMember := range encryptedGroup.MembersPendingAdminApproval { for _, requestingMember := range encryptedGroup.RequestingMembers {
if requestingMember == nil { if requestingMember == nil {
continue continue
} }
@ -520,7 +535,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember) decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember)
} }
for _, bannedMember := range encryptedGroup.MembersBanned { for _, bannedMember := range encryptedGroup.BannedMembers {
if bannedMember == nil { if bannedMember == nil {
continue 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) return nil, fmt.Errorf("failed to get group master key: %w", err)
} }
if groupMasterKey == "" { 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) return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
} }
@ -634,14 +649,14 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
Username: &groupAuth.Username, Username: &groupAuth.Username,
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Host: web.StorageHostname,
} }
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, "/v2/groups", opts) response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v2/groups", opts)
defer web.CloseBody(response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.StatusCode != 200 { 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) return cli.parseGroupResponse(ctx, response, groupMasterKey)
} }
@ -661,10 +676,6 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decrypt group: %w", err) 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 // Store the profile keys in case they're new
for _, member := range group.Members { 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) { func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds() username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{ opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username, Username: &username,
Password: &password, Password: &password,
} }
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err) 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 return decrypted, nil
} }
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, *SendEndorsementCache, error) { func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, error) {
cached, endorsement, ok := cli.GroupCache.Get(gid) cli.initGroupCache()
if ok && cached.Revision >= revision {
return cached, endorsement, nil 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) group, err := cli.fetchGroupByID(ctx, gid)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
cached, endorsement, ok = cli.GroupCache.Get(gid) cli.GroupCache.groups[gid] = group
if !ok { cli.GroupCache.lastFetched[gid] = time.Now()
zerolog.Ctx(ctx).Warn().Msg("Group not found in cache after fetching") return group, nil
return group, nil, nil
}
return cached, endorsement, nil
} }
// We should store the group master key in the group store as soon as we see it, // 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 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) { func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) {
masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey) masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey)
groupMasterKey := masterKeyFromBytes(masterKeyBytes) 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() log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger()
serverSignature := encryptedGroupChange.ServerSignature serverSignature := encryptedGroupChange.ServerSignature
encryptedActionsBytes := encryptedGroupChange.Actions 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 var err error
if verifySignature { if verifySignature {
@ -788,14 +826,17 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceUserId)) sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId))
if err != nil { if err != nil {
log.Err(err).Msg("Couldn't decrypt source serviceID") log.Err(err).Msg("Couldn't decrypt source serviceID")
return nil, err return nil, err
} }
if sourceServiceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong serviceid kind: expected aci, got pni")
}
decryptedGroupChange := &GroupChange{ decryptedGroupChange := &GroupChange{
GroupMasterKey: groupMasterKey, GroupMasterKey: groupMasterKey,
Revision: encryptedActions.Version, Revision: encryptedActions.Revision,
SourceServiceID: sourceServiceID, SourceServiceID: sourceServiceID,
} }
@ -815,7 +856,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description) descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description)
if err == nil { if err == nil {
// treat a failure in obtaining the description as non-fatal // treat a failure in obtaining the description as non-fatal
newDescription := cleanupStringProperty(descriptionBlob.GetDescriptionText()) newDescription := cleanupStringProperty(descriptionBlob.GetDescription())
decryptedGroupChange.ModifyDescription = &newDescription decryptedGroupChange.ModifyDescription = &newDescription
} }
} }
@ -850,7 +891,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if serviceID.Type != libsignalgo.ServiceIDTypeACI { if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong ServiceID kind for delete member: expected ACI, got PNI") return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
} }
decryptedGroupChange.DeleteMembers = append(decryptedGroupChange.DeleteMembers, &serviceID.UUID) decryptedGroupChange.DeleteMembers = append(decryptedGroupChange.DeleteMembers, &serviceID.UUID)
} }
@ -863,7 +904,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if serviceID.Type != libsignalgo.ServiceIDTypeACI { if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong ServiceID kind for modify member: expected ACI, got PNI") return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
} }
decryptedGroupChange.ModifyMemberRoles = append(decryptedGroupChange.ModifyMemberRoles, &RoleMember{ decryptedGroupChange.ModifyMemberRoles = append(decryptedGroupChange.ModifyMemberRoles, &RoleMember{
ACI: serviceID.UUID, ACI: serviceID.UUID,
@ -890,7 +931,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, addPendingMember := range encryptedActions.AddMembersPendingProfileKey { for _, addPendingMember := range encryptedActions.AddPendingMembers {
if addPendingMember == nil { if addPendingMember == nil {
continue continue
} }
@ -903,7 +944,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember) decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember)
} }
for _, deletePendingMember := range encryptedActions.DeleteMembersPendingProfileKey { for _, deletePendingMember := range encryptedActions.DeletePendingMembers {
if deletePendingMember == nil { if deletePendingMember == nil {
continue continue
} }
@ -916,7 +957,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID) decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID)
} }
for _, promotePendingMember := range encryptedActions.PromoteMembersPendingProfileKey { for _, promotePendingMember := range encryptedActions.PromotePendingMembers {
if promotePendingMember == nil { if promotePendingMember == nil {
continue continue
} }
@ -935,7 +976,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 // 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 { if promotePendingPniAciMember == nil {
continue continue
@ -951,7 +992,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if pniServiceID.Type != libsignalgo.ServiceIDTypePNI { if pniServiceID.Type != libsignalgo.ServiceIDTypePNI {
return nil, fmt.Errorf("wrong ServiceID kind for promote pending pni->aci: expected PNI, got ACI") return nil, fmt.Errorf("Wrong ServiceID kind: expected PNI, got ACI")
} }
decryptedGroupChange.PromotePendingPniAciMembers = append(decryptedGroupChange.PromotePendingPniAciMembers, &PromotePendingPniAciMember{ decryptedGroupChange.PromotePendingPniAciMembers = append(decryptedGroupChange.PromotePendingPniAciMembers, &PromotePendingPniAciMember{
ACI: *aci, ACI: *aci,
@ -965,7 +1006,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, addRequestingMember := range encryptedActions.AddMembersPendingAdminApproval { for _, addRequestingMember := range encryptedActions.AddRequestingMembers {
if addRequestingMember == nil { if addRequestingMember == nil {
continue continue
} }
@ -981,7 +1022,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, deleteRequestingMember := range encryptedActions.DeleteMembersPendingAdminApproval { for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers {
if deleteRequestingMember == nil { if deleteRequestingMember == nil {
continue continue
} }
@ -994,7 +1035,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID) decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID)
} }
for _, promoteRequestingMember := range encryptedActions.PromoteMembersPendingAdminApproval { for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers {
if promoteRequestingMember == nil { if promoteRequestingMember == nil {
continue continue
} }
@ -1010,7 +1051,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}) })
} }
for _, addBannedMember := range encryptedActions.AddMembersBanned { for _, addBannedMember := range encryptedActions.AddBannedMembers {
if addBannedMember == nil { if addBannedMember == nil {
continue continue
} }
@ -1027,7 +1068,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}) })
} }
for _, deleteBannedMember := range encryptedActions.DeleteMembersBanned { for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers {
if deleteBannedMember == nil { if deleteBannedMember == nil {
continue continue
} }
@ -1055,8 +1096,8 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
if encryptedActions.ModifyAnnouncementsOnly != nil { if encryptedActions.ModifyAnnouncementsOnly != nil {
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
} }
if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 { if encryptedActions.ModifyDisappearingMessagesTimer != nil && len(encryptedActions.ModifyDisappearingMessagesTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer) timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessagesTimer.Timer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1068,12 +1109,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword 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 return decryptedGroupChange, nil
} }
@ -1112,7 +1147,7 @@ func decryptPKeyAndIDorPresentation(ctx context.Context, userID []byte, profileK
return nil, nil, err return nil, nil, err
} }
if serviceID.Type == libsignalgo.ServiceIDTypePNI { if serviceID.Type == libsignalgo.ServiceIDTypePNI {
return nil, nil, fmt.Errorf("wrong serviceid kind for profile key: expected ACI, got PNI") return nil, nil, fmt.Errorf("wrong serviceid kind, expected ACI, got PNI")
} }
return &serviceID.UUID, profileKey, nil return &serviceID.UUID, profileKey, nil
@ -1127,11 +1162,11 @@ func decryptMember(ctx context.Context, member *signalpb.Member, groupSecretPara
ACI: *aci, ACI: *aci,
ProfileKey: *profileKey, ProfileKey: *profileKey,
Role: GroupMemberRole(member.Role), Role: GroupMemberRole(member.Role),
JoinedAtRevision: member.JoinedAtVersion, JoinedAtRevision: member.JoinedAtRevision,
}, nil }, 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) log := zerolog.Ctx(ctx)
encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId) encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID) userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
@ -1154,7 +1189,7 @@ func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPen
}, nil }, 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) aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1175,7 +1210,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get groupSecretParams from master key") log.Err(err).Msg("Could not get groupSecretParams from master key")
return nil, err return nil, err
} }
groupChangeActions := &signalpb.GroupChange_Actions{Version: decryptedGroupChange.Revision} groupChangeActions := &signalpb.GroupChange_Actions{Revision: decryptedGroupChange.Revision}
if decryptedGroupChange.ModifyTitle != nil { if decryptedGroupChange.ModifyTitle != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}} attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}}
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob) encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
@ -1186,7 +1221,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle} groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle}
} }
if decryptedGroupChange.ModifyDescription != nil { 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) encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get encrypt description") log.Err(err).Msg("Could not get encrypt description")
@ -1208,7 +1243,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
JoinFromInviteLink: addMember.JoinFromInviteLink, JoinFromInviteLink: addMember.JoinFromInviteLink,
}) })
} else { } else {
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{ groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember, Added: encryptedPendingMember,
}) })
} }
@ -1240,7 +1275,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Failed to encrypt pendingMember") log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{ groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember, Added: encryptedPendingMember,
}) })
} }
@ -1250,7 +1285,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deletePendingMember") log.Err(err).Msg("Encrypt UserId error for deletePendingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersPendingProfileKey = append(groupChangeActions.DeleteMembersPendingProfileKey, &signalpb.GroupChange_Actions_DeleteMemberPendingProfileKeyAction{ groupChangeActions.DeletePendingMembers = append(groupChangeActions.DeletePendingMembers, &signalpb.GroupChange_Actions_DeletePendingMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1268,7 +1303,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember") log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err return nil, err
} }
groupChangeActions.PromoteMembersPendingProfileKey = append(groupChangeActions.PromoteMembersPendingProfileKey, &signalpb.GroupChange_Actions_PromoteMemberPendingProfileKeyAction{ groupChangeActions.PromotePendingMembers = append(groupChangeActions.PromotePendingMembers, &signalpb.GroupChange_Actions_PromotePendingMemberAction{
Presentation: *presentation, Presentation: *presentation,
}) })
} }
@ -1286,8 +1321,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember") log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersPendingAdminApproval = append(groupChangeActions.AddMembersPendingAdminApproval, &signalpb.GroupChange_Actions_AddMemberPendingAdminApprovalAction{ groupChangeActions.AddRequestingMembers = append(groupChangeActions.AddRequestingMembers, &signalpb.GroupChange_Actions_AddRequestingMemberAction{
Added: &signalpb.MemberPendingAdminApproval{ Added: &signalpb.RequestingMember{
Presentation: *presentation, Presentation: *presentation,
}, },
}) })
@ -1298,7 +1333,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember") log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersPendingAdminApproval = append(groupChangeActions.DeleteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_DeleteMemberPendingAdminApprovalAction{ groupChangeActions.DeleteRequestingMembers = append(groupChangeActions.DeleteRequestingMembers, &signalpb.GroupChange_Actions_DeleteRequestingMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1309,7 +1344,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return nil, err return nil, err
} }
groupChangeActions.PromoteMembersPendingAdminApproval = append(groupChangeActions.PromoteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_PromoteMemberPendingAdminApprovalAction{ groupChangeActions.PromoteRequestingMembers = append(groupChangeActions.PromoteRequestingMembers, &signalpb.GroupChange_Actions_PromoteRequestingMemberAction{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
Role: signalpb.Member_Role(promoteRequestingMember.Role), Role: signalpb.Member_Role(promoteRequestingMember.Role),
}) })
@ -1320,8 +1355,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember") log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersBanned = append(groupChangeActions.AddMembersBanned, &signalpb.GroupChange_Actions_AddMemberBannedAction{ groupChangeActions.AddBannedMembers = append(groupChangeActions.AddBannedMembers, &signalpb.GroupChange_Actions_AddBannedMemberAction{
Added: &signalpb.MemberBanned{ Added: &signalpb.BannedMember{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
Timestamp: addBannedMember.Timestamp, Timestamp: addBannedMember.Timestamp,
}, },
@ -1333,7 +1368,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember") log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersBanned = append(groupChangeActions.DeleteMembersBanned, &signalpb.GroupChange_Actions_DeleteMemberBannedAction{ groupChangeActions.DeleteBannedMembers = append(groupChangeActions.DeleteBannedMembers, &signalpb.GroupChange_Actions_DeleteBannedMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1364,7 +1399,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get encrypt Title") log.Err(err).Msg("Could not get encrypt Title")
return nil, err return nil, err
} }
groupChangeActions.ModifyDisappearingMessageTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessageTimerAction{Timer: *encryptedTimer} groupChangeActions.ModifyDisappearingMessagesTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessagesTimerAction{Timer: *encryptedTimer}
} }
if decryptedGroupChange.ModifyInviteLinkPassword != nil { if decryptedGroupChange.ModifyInviteLinkPassword != nil {
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword) inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword)
@ -1379,7 +1414,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil) 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) log := zerolog.Ctx(ctx)
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI) expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI)
if err != nil { if err != nil {
@ -1407,7 +1442,7 @@ func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, group
return &encryptedMember, nil, nil 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) log := zerolog.Ctx(ctx)
encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID) encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID)
if err != nil { if err != nil {
@ -1419,7 +1454,7 @@ func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *Pend
log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember") log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember")
return nil, err return nil, err
} }
encryptedPendingMember := signalpb.MemberPendingProfileKey{ encryptedPendingMember := signalpb.PendingMember{
AddedByUserId: encryptedAddedByUserID[:], AddedByUserId: encryptedAddedByUserID[:],
Member: &signalpb.Member{ Member: &signalpb.Member{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
@ -1472,9 +1507,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Body: requestBody, Body: requestBody,
Host: web.StorageHostname,
} }
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPatch, path, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodPatch, path, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) return nil, fmt.Errorf("SendRequest error: %w", err)
} }
@ -1513,21 +1548,17 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
return &changeResp, nil 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) { func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger() log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid) groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to get master key for group: %w", err) return 0, fmt.Errorf("failed to get master key for group: %w", err)
} else if groupMasterKey == "" {
return 0, ErrGroupMasterKeyNotFound
} }
groupChange.GroupMasterKey = groupMasterKey groupChange.GroupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChangeResponse var signedGroupChange *signalpb.GroupChangeResponse
group, _, err := cli.RetrieveGroupByID(ctx, gid, 0) group, err := cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch group info to update: %w", err) return 0, fmt.Errorf("failed to fetch group info to update: %w", err)
} }
@ -1550,8 +1581,10 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("failed to update group: %w", err) return 0, fmt.Errorf("failed to update group: %w", err)
} }
} else if errors.Is(err, ConflictError) { } else if errors.Is(err, ConflictError) {
cli.GroupCache.Delete(gid) delete(cli.GroupCache.groups, gid)
group, _, err = cli.RetrieveGroupByID(ctx, gid, 0) delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
group, err = cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch group after conflict: %w", err) return 0, fmt.Errorf("failed to fetch group after conflict: %w", err)
} }
@ -1564,13 +1597,12 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err) 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 { if signedGroupChange == nil {
return 0, fmt.Errorf("no signed group change returned: %w", err) 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) groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to marshal signed group change: %w", err) return 0, fmt.Errorf("failed to marshal signed group change: %w", err)
@ -1603,12 +1635,12 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
encryptedGroup := &signalpb.Group{ encryptedGroup := &signalpb.Group{
PublicKey: groupPublicParams[:], PublicKey: groupPublicParams[:],
Title: *encryptedTitle, Title: *encryptedTitle,
AvatarUrl: decryptedGroup.AvatarPath, Avatar: decryptedGroup.AvatarPath,
AnnouncementsOnly: decryptedGroup.AnnouncementsOnly, AnnouncementsOnly: decryptedGroup.AnnouncementsOnly,
Version: 0, Revision: 0,
} }
if decryptedGroup.Description != "" { 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) encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get encrypt Description") log.Err(err).Msg("Could not get encrypt Description")
@ -1634,7 +1666,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
if encryptedMember != nil { if encryptedMember != nil {
encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember) encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember)
} else { } else {
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember) encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
} }
} }
for _, pendingMember := range decryptedGroup.PendingMembers { for _, pendingMember := range decryptedGroup.PendingMembers {
@ -1643,7 +1675,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
log.Err(err).Msg("Failed to encrypt pendingMember") log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err return nil, err
} }
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember) encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
} }
return encryptedGroup, nil return encryptedGroup, nil
} }
@ -1666,7 +1698,7 @@ func PrepareGroupCreation(decryptedGroup *Group) (libsignalgo.GroupMasterKey, er
return masterKeyBytes, nil 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() log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger()
masterKeyBytes, err := PrepareGroupCreation(decryptedGroup) masterKeyBytes, err := PrepareGroupCreation(decryptedGroup)
if err != nil { if err != nil {
@ -1681,6 +1713,14 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error") log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err 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) encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to encrypt group") log.Err(err).Msg("Failed to encrypt group")
@ -1702,9 +1742,9 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Body: requestBody, Body: requestBody,
Host: web.StorageHostname,
} }
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPut, path, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodPut, path, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) return nil, fmt.Errorf("SendRequest error: %w", err)
} }
@ -1731,9 +1771,9 @@ func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
return InviteLinkPasswordFromBytes(random.Bytes(16)) 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() 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 { if err != nil {
log.Err(err).Msg("Error creating group on server") log.Err(err).Msg("Error creating group on server")
return nil, err return nil, err
@ -1756,7 +1796,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
return nil, err return nil, err
} }
if groupMasterKey == "" { if groupMasterKey == "" {
return nil, ErrGroupMasterKeyNotFound return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
} }
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
@ -1767,20 +1807,16 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
Username: &groupAuth.Username, Username: &groupAuth.Username,
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Headers: map[string]string{ Host: web.StorageHostname,
// TODO actually cache the data and provide real expiry timestamp
"Cached-Send-Endorsements": "0",
},
} }
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false // 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) 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) response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts)
defer web.CloseBody(response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.StatusCode != 200 { 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 var encryptedGroupChanges signalpb.GroupChanges
groupChangesBytes, err := io.ReadAll(response.Body) groupChangesBytes, err := io.ReadAll(response.Body)

View file

@ -22,7 +22,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math/rand/v2" "math/rand"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -43,6 +43,25 @@ type GeneratedPreKeys struct {
IdentityKey []uint8 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 { func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore) error {
var identityKeyPair *libsignalgo.IdentityKeyPair var identityKeyPair *libsignalgo.IdentityKeyPair
var pni bool var pni bool
@ -78,11 +97,10 @@ func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore
KyberPreKeys: kyberPreKeys, KyberPreKeys: kyberPreKeys,
IdentityKey: identityKey, IdentityKey: identityKey,
} }
zerolog.Ctx(ctx).Debug(). preKeyUsername := fmt.Sprintf("%s.%d", cli.Store.ACI, cli.Store.DeviceID)
Int("num_prekeys", len(preKeys)). log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
Int("num_kyber_prekeys", len(kyberPreKeys)). log.Debug().Int("num_prekeys", len(preKeys)).Int("num_kyber_prekeys", len(kyberPreKeys)).Msg("Registering prekeys")
Msg("Registering all prekeys") err = RegisterPreKeys(ctx, &generatedPreKeys, pni, preKeyUsername, cli.Store.Password)
err = cli.RegisterPreKeys(ctx, &generatedPreKeys, pni)
if err != nil { if err != nil {
return fmt.Errorf("failed to register prekeys: %w", err) 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") 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() log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
// Convert generated prekeys to JSON // Convert generated prekeys to JSON
preKeysJson := []map[string]any{} preKeysJson := []map[string]interface{}{}
kyberPreKeysJson := []map[string]any{} kyberPreKeysJson := []map[string]interface{}{}
for _, preKey := range generatedPreKeys.PreKeys { for _, preKey := range generatedPreKeys.PreKeys {
preKeyJson, err := PreKeyToJSON(preKey) preKeyJson, err := PreKeyToJSON(preKey)
if err != nil { if err != nil {
@ -349,27 +367,32 @@ func (cli *Client) RegisterPreKeys(ctx context.Context, generatedPreKeys *Genera
} }
identityKey := generatedPreKeys.IdentityKey identityKey := generatedPreKeys.IdentityKey
registerJSON := map[string]any{ register_json := map[string]interface{}{
"preKeys": preKeysJson, "preKeys": preKeysJson,
"pqPreKeys": kyberPreKeysJson, "pqPreKeys": kyberPreKeysJson,
"identityKey": base64.StdEncoding.EncodeToString(identityKey), "identityKey": base64.StdEncoding.EncodeToString(identityKey),
} }
// Send request // Send request
jsonBytes, err := json.Marshal(registerJSON) jsonBytes, err := json.Marshal(register_json)
if err != nil { if err != nil {
log.Err(err).Msg("Error marshalling register JSON") log.Err(err).Msg("Error marshalling register JSON")
return err 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 { if err != nil {
log.Err(err).Msg("Error sending request") log.Err(err).Msg("Error sending request")
return err return err
} }
if resp.GetStatus() == 422 { defer resp.Body.Close()
// status code not 2xx
if resp.StatusCode == 422 {
return errPrekeyUpload422 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 { type prekeyResponse struct {
@ -404,40 +427,25 @@ func addBase64PaddingAndDecode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data) 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 { 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 // Fetch prekey
deviceIDPath := "/*" deviceIDPath := "/*"
if specificDeviceID >= 0 { if specificDeviceID >= 0 {
deviceIDPath = "/" + fmt.Sprint(specificDeviceID) deviceIDPath = "/" + fmt.Sprint(specificDeviceID)
} }
// TODO this should be done via the unauthed websocket if possible
path := "/v2/keys/" + theirServiceID.String() + deviceIDPath + "?pq=true" 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 { if err != nil {
return fmt.Errorf("error sending request: %w", err) 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 var prekeyResponse prekeyResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp) err = web.DecodeHTTPResponseBody(ctx, &prekeyResponse, resp)
if err != nil { if err != nil {
return fmt.Errorf("error decoding response body: %w", err) return fmt.Errorf("error decoding response body: %w", err)
} }
rawIdentityKey, err := addBase64PaddingAndDecode(respData.IdentityKey) rawIdentityKey, err := addBase64PaddingAndDecode(prekeyResponse.IdentityKey)
if err != nil { if err != nil {
return fmt.Errorf("error decoding identity key: %w", err) 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) // 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 publicKey *libsignalgo.PublicKey
var preKeyID uint32 var preKeyID uint32
if d.PreKey != nil { if d.PreKey != nil {
@ -522,7 +530,6 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
ctx, ctx,
preKeyBundle, preKeyBundle,
address, address,
localAddress,
cli.Store.ACISessionStore, cli.Store.ACISessionStore,
cli.Store.ACIIdentityStore, cli.Store.ACIIdentityStore,
) )
@ -548,18 +555,19 @@ func keysPath(pni bool) string {
func (cli *Client) GetMyKeyCounts(ctx context.Context, pni bool) (int, int, error) { func (cli *Client) GetMyKeyCounts(ctx context.Context, pni bool) (int, int, error) {
log := zerolog.Ctx(ctx).With().Str("action", "get my key counts").Logger() 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 { if err != nil {
log.Err(err).Msg("Error sending request") log.Err(err).Msg("Error sending request")
return 0, 0, err return 0, 0, err
} }
var respData preKeyCountResponse var preKeyCountResponse preKeyCountResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp) err = web.DecodeHTTPResponseBody(ctx, &preKeyCountResponse, resp)
if err != nil { if err != nil {
log.Err(err).Msg("Fetching prekey counts, error with response body") log.Err(err).Msg("Fetching prekey counts, error with response body")
return 0, 0, err 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 { 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() log := zerolog.Ctx(ctx).With().Str("action", "start key check loop").Logger()
// Do the initial check in 5-10 minutes after starting the loop // Do the initial check in 5-10 minutes after starting the loop
windowStart := 0 window_start := 0
windowSize := 1 window_size := 1
firstRun := true
for { for {
randomMinutesInWindow := rand.IntN(windowSize) + windowStart random_minutes_in_window := rand.Intn(window_size) + window_start
checkTime := time.Duration(randomMinutesInWindow) * time.Minute check_time := time.Duration(random_minutes_in_window) * time.Minute
if firstRun { log.Debug().Dur("check_time", check_time).Msg("Waiting to check for new prekeys")
checkTime = 0
firstRun = false
} else {
log.Debug().Dur("check_time", checkTime).Msg("Waiting to check for new prekeys")
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(checkTime): case <-time.After(check_time):
err := cli.CheckAndUploadNewPreKeys(ctx, cli.Store.ACIPreKeyStore) err := cli.CheckAndUploadNewPreKeys(ctx, cli.Store.ACIPreKeyStore)
if err != nil { if err != nil {
log.Err(err).Msg("Error checking and uploading new prekeys for ACI identity") log.Err(err).Msg("Error checking and uploading new prekeys for ACI identity")
// Retry within half an hour // Retry within half an hour
windowStart = 5 window_start = 5
windowSize = 25 window_size = 25
continue continue
} }
err = cli.CheckAndUploadNewPreKeys(ctx, cli.Store.PNIPreKeyStore) 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") log.Err(err).Msg("Error checking and uploading new prekeys for PNI identity")
// Retry within half an hour // Retry within half an hour
windowStart = 5 window_start = 5
windowSize = 25 window_size = 25
continue continue
} }
// After a successful check, check again in 36 to 60 hours // After a successful check, check again in 36 to 60 hours
windowStart = 36 * 60 window_start = 36 * 60
windowSize = 24 * 60 window_size = 24 * 60
} }
} }
} }

View file

@ -18,10 +18,7 @@ package signalmeow
import ( import (
_ "embed" _ "embed"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exerrors" "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) Flush() {}
func (FFILogger) Destroy() {}
// Ensure FFILogger implements the Logger interface // Ensure FFILogger implements the Logger interface
var _ libsignalgo.Logger = FFILogger{} var _ libsignalgo.Logger = FFILogger{}
@ -81,28 +76,3 @@ var prodServerPublicParams *libsignalgo.ServerPublicParams
func init() { func init() {
prodServerPublicParams = exerrors.Must(libsignalgo.DeserializeServerPublicParams(prodServerPublicParamsSlice)) 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) { 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 // 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 // or if we have a cached error that is less than an hour old
profile, err := cli.getCachedProfileByID(signalID, refreshAfter) profile, err := cli.getCachedProfileByID(signalID, refreshAfter)
@ -210,18 +218,18 @@ func (cli *Client) fetchProfileWithRequestAndKey(ctx context.Context, signalID u
path += "/" + string(credentialRequest) path += "/" + string(credentialRequest)
path += "?credentialType=expiringProfileKey" path += "?credentialType=expiringProfileKey"
} }
headers := http.Header{} profileRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
if useUnidentified { if useUnidentified {
headers.Set("Unidentified-Access-Key", base64AccessKey) profileRequest.Headers = append(profileRequest.Headers, "unidentified-access-key:"+base64AccessKey)
headers.Set("Accept-Language", "en-US") 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 { if err != nil {
return nil, fmt.Errorf("error sending request: %w", err) return nil, fmt.Errorf("error sending request: %w", err)
} }
var profile types.Profile var profile types.Profile
profile.FetchedAt = time.Now() 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 logEvt.Enabled() {
if json.Valid(resp.Body) { if json.Valid(resp.Body) {
logEvt.RawJSON("response_data", 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) { func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey libsignalgo.ProfileKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds() username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{ opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username, Username: &username,
Password: &password, 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 { if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err) 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 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) 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
}

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