Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-08-23 12:24: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
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!signal version` command output is: ``
It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
ignored or closed immediately.
-->

View file

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

View file

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

View file

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

View file

@ -1,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)
* Removed legacy provisioning API and database legacy migration.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

45
go.mod
View file

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

88
go.sum
View file

@ -1,18 +1,19 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@ -42,46 +47,45 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10 h1:EvX/di02gOriKN0xGDJuQ5mgiNdAF4LJc8moffI7Svo=
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/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -91,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=
maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf h1:prmIYgiziW4A8H2v/TliQ7fis8uTWblabxyPIeLFlNg=
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 {
continue
}
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, senderACI, s.Main.Bridge.Bot, dm, attMap)
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, s.Main.Bridge.Bot, dm, attMap)
convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions))
for _, reaction := range reactions {
reactionSenderACI, err := getRecipientACI(reaction.AuthorId)
@ -187,7 +187,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
CompleteCallback: func() {
// When reaching the last backwards backfill batch, delete the chat from the backup store.
// If backwards backfilling isn't enabled, delete immediately after the first backfill request.
if (!params.Forward && len(items) < params.Count) || !s.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
if (!params.Forward && len(items) < params.Count) || (!s.Main.Bridge.Config.Backfill.Queue.Enabled && !s.Main.Bridge.Config.Backfill.WillPaginateManually) {
err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")

View file

@ -25,7 +25,6 @@ import (
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
@ -38,7 +37,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.signal.capabilities.2026_07_22"
base := "fi.mau.signal.capabilities.2025_08_25"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -77,7 +76,6 @@ var signalCaps = &event.RoomFeatures{
"image/jpeg": event.CapLevelFullySupported,
"image/webp": event.CapLevelFullySupported,
"image/bmp": event.CapLevelFullySupported,
"image/avif": event.CapLevelFullySupported,
},
MaxWidth: 4096,
MaxHeight: 4096,
@ -99,8 +97,6 @@ var signalCaps = &event.RoomFeatures{
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/aac": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported,
},
MaxSize: MaxFileSize,
},
@ -114,8 +110,7 @@ var signalCaps = &event.RoomFeatures{
},
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
// Signal clients will only render static webp, so apng is preferred
"image/webp": event.CapLevelPartialSupport,
"image/webp": event.CapLevelFullySupported,
"image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(),
@ -141,27 +136,9 @@ var signalCaps = &event.RoomFeatures{
MaxSize: MaxFileSize,
},
},
State: event.StateFeatureMap{
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
},
MemberActions: event.MemberFeatureMap{
event.MemberActionInvite: event.CapLevelFullySupported,
event.MemberActionRevokeInvite: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported,
event.MemberActionBan: event.CapLevelFullySupported,
event.MemberActionKick: event.CapLevelFullySupported,
},
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelFullySupported,
PollMaxOptions: 10,
PollOptionMaxLength: 100,
Poll: event.CapLevelRejected,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
@ -178,12 +155,6 @@ var signalCaps = &event.RoomFeatures{
CustomEmojiReactions: false,
ReadReceipts: true,
TypingNotifications: true,
DeleteChat: true,
MessageRequest: &event.MessageRequestFeatures{
AcceptWithMessage: event.CapLevelPartialSupport,
AcceptWithButton: event.CapLevelFullySupported,
},
}
var signalDisappearingCap = &event.DisappearingTimerCapability{
@ -191,16 +162,9 @@ var signalDisappearingCap = &event.DisappearingTimerCapability{
}
var signalCapsNoteToSelf *event.RoomFeatures
var signalCapsDM *event.RoomFeatures
func init() {
signalCapsDM = ptr.Clone(signalCaps)
signalCapsDM.ID = capID() + "+dm"
signalCapsDM.MemberActions = nil
signalCapsDM.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
}
signalCapsNoteToSelf = ptr.Clone(signalCapsDM)
signalCapsNoteToSelf = ptr.Clone(signalCaps)
signalCapsNoteToSelf.EditMaxAge = nil
signalCapsNoteToSelf.DeleteMaxAge = nil
signalCapsNoteToSelf.ID = capID() + "+note_to_self"
@ -209,8 +173,6 @@ func init() {
func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
return signalCapsNoteToSelf
} else if portal.RoomType == database.RoomTypeDM {
return signalCapsDM
}
return signalCaps
}
@ -220,7 +182,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@ -245,5 +206,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
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.GroupCreatingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.GhostDMCreatingNetworkAPI = (*SignalClient)(nil)
)
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
const PrivateChatTopic = "Signal private chat"
const NoteToSelfName = "Signal Note to Self"
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 {
return nil, err
}
@ -62,17 +59,12 @@ func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *b
// Don't do unnecessary fetches in background mode
return nil, nil
}
var contact *types.Recipient
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)
}
contact, err := s.Client.ContactByACIWithRefreshAfter(ctx, userID, refreshAfter)
if err != nil {
return nil, err
}
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 s.contactToUserInfo(ctx, contact)
@ -165,41 +157,18 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
return ui, nil
}
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
_, err := signalid.ParseUserIDAsServiceID(id)
return err == nil
}
func (s *SignalClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, 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) {
func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
var aci, pni uuid.UUID
var e164Number uint64
var recipient *types.Recipient
serviceID, err := signalid.ParseUserIDAsServiceID(networkid.UserID(number))
serviceID, err := libsignalgo.ServiceIDFromString(number)
if err != nil {
number, err = bridgev2.CleanPhoneNumber(number)
if err != nil {
@ -212,7 +181,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
return nil, fmt.Errorf("error looking up number in local contact list: %w", err)
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
} else if recipient != nil {
aci = recipient.ACI
pni = recipient.PNI
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
@ -228,9 +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")
}
aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
}
} else {
aci, pni = serviceID.ToACIAndPNI()
@ -250,29 +216,31 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
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
resp := &bridgev2.ResolveIdentifierResponse{
UserID: userID,
UserInfo: userInfo,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}
resp.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, resp.UserID)
if aci != uuid.Nil {
ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(aci))
if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err)
}
return resp, nil
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
}
}
func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
group := &signalmeow.Group{
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,
AnnouncementsOnly: false,
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,
Role: signalmeow.GroupMember_ADMINISTRATOR,
}
currentTS := uint64(time.Now().UnixMilli())
for _, member := range params.Participants {
userID, err := signalid.ParseUserIDAsServiceID(member)
for i, member := range params.Participants {
userID, err := signalid.ParseUserID(member)
if err != nil {
return nil, fmt.Errorf("invalid user ID %q: %w", member, err)
}
if userID.Type == libsignalgo.ServiceIDTypeACI {
group.Members = append(group.Members, &signalmeow.GroupMember{
ACI: userID.UUID,
group.Members[i+1] = &signalmeow.GroupMember{
ACI: userID,
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)
@ -326,13 +283,13 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
}
var avatarBytes []byte
var avatarMXC id.ContentURIString
if params.Avatar != nil && params.Avatar.URL != "" {
if params.Avatar != nil {
avatarMXC = params.Avatar.URL
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil)
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, params.Avatar.MSC3414File)
if err != nil {
return nil, fmt.Errorf("failed to download avatar: %w", err)
}
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to upload avatar: %w", err)
}
@ -362,7 +319,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
return nil, fmt.Errorf("failed to set portal room ID: %w", err)
}
}
resp, err := s.Client.CreateGroup(ctx, group)
resp, err := s.Client.CreateGroup(ctx, group, avatarBytes)
if err != nil {
return nil, fmt.Errorf("failed to create group: %w", err)
}
@ -414,7 +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 {
namePtr := bridgev2.DefaultChatName
name := ""
topic := PrivateChatTopic
selfUser := s.makeEventSender(s.Client.Store.ACI)
members := &bridgev2.ChatMemberList{
@ -441,7 +398,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
var serviceID libsignalgo.ServiceID
var avatar *bridgev2.Avatar
if recipient.ACI == uuid.Nil {
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient))
name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else {
if backupChat == nil {
@ -453,7 +410,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
}
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI {
namePtr = ptr.Ptr(NoteToSelfName)
name = NoteToSelfName
avatar = &bridgev2.Avatar{
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
@ -474,14 +431,14 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
return &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(serviceID),
PortalInfo: &bridgev2.ChatInfo{
Name: namePtr,
Name: &name,
Avatar: avatar,
Topic: &topic,
Members: members,
Type: ptr.Ptr(database.RoomTypeDM),
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
CanBackfill: backupChat != nil,
ExtraUpdates: updatePortalSyncMeta,
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ func inviteLinkToJoinRule(inviteLinkAccess signalmeow.AccessControl) event.JoinR
}
func (s *SignalClient) getGroupInfo(ctx context.Context, groupID types.GroupIdentifier, minRevision uint32, backupChat *store.BackupChat) (*bridgev2.ChatInfo, error) {
groupInfo, _, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
groupInfo, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
if err != nil {
return nil, fmt.Errorf("failed to retrieve group by id: %w", err)
}
@ -123,30 +123,43 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
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 {
members.MemberMap.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
evtSender := s.makeEventSender(member.ACI)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
PowerLevel: roleToPL(member.Role),
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 {
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
evtSender := s.makeEventSender(*aci)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
Membership: event.MembershipBan,
})
}
}
if backupChat == nil {
var err error
@ -178,10 +191,6 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
}, 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 {
meta := portal.Metadata.(*signalid.PortalMetadata)
meta.LastSync = jsontime.UnixNow()
@ -277,127 +286,131 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess),
}
}
mc := make(bridgev2.ChatMemberMap)
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,
})
}
var mc []bridgev2.ChatMember
for _, member := range groupChange.AddMembers {
mc.Set(bridgev2.ChatMember{
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin,
})
}
for _, member := range groupChange.ModifyMemberRoles {
mc.Set(bridgev2.ChatMember{
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role),
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 {
ic.MemberChanges = &bridgev2.ChatMemberList{MemberMap: mc, PowerLevels: pls}
ic.MemberChanges = &bridgev2.ChatMemberList{Members: mc, PowerLevels: pls}
}
return ic, nil
}
func (s *SignalClient) addChatMemberWithACIQuery(
ctx context.Context, mc bridgev2.ChatMemberMap, serviceID libsignalgo.ServiceID, member bridgev2.ChatMember,
) {
member.EventSender = s.makeEventSenderFromServiceID(serviceID)
mc.Set(member)
if aci := s.tryResolvePNItoLoggedInACI(ctx, serviceID); aci != nil {
member.EventSender = s.makeEventSender(*aci)
mc.Add(member)
func (s *SignalClient) maybeResolvePNItoACI(ctx context.Context, serviceID *libsignalgo.ServiceID) *uuid.UUID {
if serviceID.Type == libsignalgo.ServiceIDTypeACI {
return &serviceID.UUID
}
}
func (s *SignalClient) tryResolvePNItoLoggedInACI(ctx context.Context, serviceID libsignalgo.ServiceID) *uuid.UUID {
if serviceID.Type != libsignalgo.ServiceIDTypePNI {
device, err := s.Client.Store.DeviceStore.DeviceByPNI(ctx, serviceID.UUID)
if err != nil || device == 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
}
}
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)
if err != nil {
log.Err(err).Msg("Failed to convert group info")
} else {
portal.ProcessChatInfoChange(ctx, s.makeEventSenderFromServiceID(gc.GroupChange.SourceServiceID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
} else if gc.GroupChange.SourceServiceID.Type == libsignalgo.ServiceIDTypeACI {
portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceServiceID.UUID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
}
if gc.GroupChange.Revision == toRevision {
break

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -84,7 +84,8 @@ func (i *IdentityKey) VerifyAlternateIdentity(other *IdentityKey, signature []by
}
func (i *IdentityKey) Equal(other *IdentityKey) (bool, error) {
return i.publicKey.Equal(other.publicKey)
result, err := i.publicKey.Compare(other.publicKey)
return result == 0, err
}
type IdentityKeyPair struct {
@ -113,13 +114,14 @@ func GenerateIdentityKeyPair() (*IdentityKeyPair, error) {
}
func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) {
var keys C.SignalPairOfMutPointerPublicKeyMutPointerPrivateKey
signalFfiError := C.signal_identitykeypair_deserialize(&keys, BytesToBuffer(bytes))
var privateKey C.SignalMutPointerPrivateKey
var publicKey C.SignalMutPointerPublicKey
signalFfiError := C.signal_identitykeypair_deserialize(&privateKey, &publicKey, BytesToBuffer(bytes))
runtime.KeepAlive(bytes)
if signalFfiError != nil {
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) {

View file

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

View file

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

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,33 +48,19 @@ func ParseUserLoginID(userLoginID networkid.UserLoginID) (uuid.UUID, error) {
return userID, nil
}
func toServiceID(id uuid.UUID, err error) (libsignalgo.ServiceID, error) {
if err != nil {
return libsignalgo.ServiceID{}, err
}
return libsignalgo.NewACIServiceID(id), nil
}
func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (libsignalgo.ServiceID, error) {
func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (uuid.UUID, error) {
switch ghostOrUserLogin := ghostOrUserLogin.(type) {
case *bridgev2.UserLogin:
return toServiceID(ParseUserLoginID(ghostOrUserLogin.ID))
return ParseUserLoginID(ghostOrUserLogin.ID)
case *bridgev2.Ghost:
return ParseUserIDAsServiceID(ghostOrUserLogin.ID)
return ParseUserID(ghostOrUserLogin.ID)
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) {
userIDStr := string(userID)
if strings.HasPrefix(userIDStr, pniUserIDPrefix) {
userIDStr = pniServiceIDPrefix + userIDStr[len(pniUserIDPrefix):]
}
return libsignalgo.ServiceIDFromString(userIDStr)
return libsignalgo.ServiceIDFromString(string(userID))
}
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 {
switch user.Type {
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))
}
return networkid.UserID(user.String())
}
func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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