1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-14 13:16:54 -04:00

Compare commits

..

1 commit

Author SHA1 Message Date
Tulir Asokan
92ff478b1f capabilities: advertise supported state events and member actions 2025-10-27 15:02:37 +02:00
121 changed files with 4491 additions and 10979 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,68 +1,8 @@
# v26.04
# v25.11 (unreleased)
* Updated libsignal to v0.92.1
* Added support for admin message deletes from Signal.
* Added support for binary service IDs in storage service.
* Fixed `private_chat_portal_meta` option not setting DM room names correctly.
* Fixed panic if user is logged out during initial chat sync.
* Fixed avatar upload failing when creating new Signal group.
# v26.03
* Switched to sending binary service ID fields in outgoing messages.
* Added support for roundtripping large attachments via disk to avoid keeping
the entire file in memory during en/decryption.
# v26.02.2
* Added support for more new binary service ID fields.
# v26.02.1
* Updated libsignal to v0.87.5.
* Added support for new binary service ID fields that Signal 8.0 switched to.
# v26.02
* Bumped minimum Go version to 1.25.
* Updated libsignal to v0.87.1.
* Added automatic recovery for the session not found error from libsignal.
* Fixed sender key state not being cleared on logout properly.
# v26.01
* Updated libsignal to v0.86.12.
* Changed automatic contact list sync option to only sync every 3 days rather
than on every restart.
* Fixed sending messages to groups with no other registered members.
* Fixed sender key sends failing if some users had changed devices.
* Fixed timestamps of outgoing typing notifications in DMs.
# v25.12
* Updated libsignal to v0.86.8.
* Updated Docker image to Alpine 3.23.
* Added support for dropping incoming DMs from blocked contacts on Signal.
* Added support for sender key encryption when sending to groups, which makes
sending much faster and enables sending typing notifications.
* Added support for encryption retry receipts.
* Fixed bugs with handling poll votes.
* Fixed history transfer option not showing up when pairing with Signal Android.
* Fixed nicknames being cleared not being bridged
(thanks to [@Enzime] in [#623]).
[#623]: https://github.com/mautrix/signal/pull/623
[@Enzime]: https://github.com/Enzime
# v25.11
* Updated libsignal to v0.86.4.
* Added support for bridging invite state in groups for phone number invites.
* Added support for polls.
* Fixed PNI signature not being sent when replying to message requests.
* Fixed unnecessary repeating error notices when Signal is down.
* Fixed sticker size metadata on Matrix not matching how native Signal Desktop
renders them.
# v25.10

View file

@ -1,17 +1,18 @@
# -- Build libsignal (with Rust) --
FROM rust:1-alpine AS rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev protobuf-dev
FROM rust:1-alpine as rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
WORKDIR /build
# Copy all files needed for Rust build, and no Go files
COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
COPY build-rust.sh .
ARG DBG=0
RUN ./build-rust.sh
# -- Build mautrix-signal (with Go) --
FROM golang:1-alpine3.23 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.23
FROM alpine:3.22
ENV UID=1337 \
GID=1337
@ -41,6 +48,11 @@ RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go olm
COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal
COPY --from=go-builder /build/docker-run.sh /docker-run.sh
COPY --from=go-builder /go/bin/dlv /usr/bin/dlv
VOLUME /data
ARG DBG
ARG DBGWAIT=0
ENV DBG=${DBG} DBGWAIT=${DBGWAIT}
RUN echo "Debug mode: DBG=${DBG} DBGWAIT=${DBGWAIT}"
CMD ["/docker-run.sh"]

View file

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

View file

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

View file

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

View file

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

View file

@ -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,13 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.",
Version: "26.04",
Version: "25.10",
SemCalVer: true,
Connector: &connector.SignalConnector{},
}
func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)

41
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.2
tool go.mau.fi/util/cmd/maubuild
toolchain go1.25.3
require (
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.18.0
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
go.mau.fi/util v0.9.2
golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b
golang.org/x/net v0.46.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
maunium.net/go/mautrix v0.25.3-0.20251027130059-9f2669025f28
)
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.44 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // 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.2 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect

76
go.sum
View file

@ -1,16 +1,17 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -22,19 +23,23 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/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,8 +47,8 @@ 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=
@ -51,37 +56,36 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
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.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM=
go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -91,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
maunium.net/go/mautrix v0.25.3-0.20251027130059-9f2669025f28 h1:BG0IPWqlVR2mxAQM8jhOhHa168si5iXKeTv11nCGj58=
maunium.net/go/mautrix v0.25.3-0.20251027130059-9f2669025f28/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI=

View file

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

View file

@ -38,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.signal.capabilities.2026_05_12"
base := "fi.mau.signal.capabilities.2025_10_27"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -111,8 +111,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(),
@ -139,10 +138,10 @@ var signalCaps = &event.RoomFeatures{
},
},
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},
event.StateRoomName.Type: event.CapLevelFullySupported,
event.StateRoomAvatar.Type: event.CapLevelFullySupported,
event.StateTopic.Type: event.CapLevelFullySupported,
event.StateBeeperDisappearingTimer.Type: event.CapLevelFullySupported,
},
MemberActions: event.MemberFeatureMap{
event.MemberActionInvite: event.CapLevelFullySupported,
@ -170,12 +169,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{
@ -190,7 +183,7 @@ func init() {
signalCapsDM.ID = capID() + "+dm"
signalCapsDM.MemberActions = nil
signalCapsDM.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
event.StateBeeperDisappearingTimer.Type: event.CapLevelFullySupported,
}
signalCapsNoteToSelf = ptr.Clone(signalCapsDM)
signalCapsNoteToSelf.EditMaxAge = nil
@ -212,7 +205,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@ -237,5 +229,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 8
return 1, 6
}

View file

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

View file

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

View file

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

View file

@ -42,7 +42,6 @@ type SignalConfig struct {
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
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)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
}
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 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)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
}
case *signalid.DirectMediaSticker:
log.Info().
Hex("pack_id", info.PackID).
Uint32("sticker_id", info.StickerID).
Msg("Direct downloading sticker")
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 0, err
}
rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, err
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
}
if rawDataResp == nil {
return nil, fmt.Errorf("unexpected fallthrough with no data")
}
return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
}

View file

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

View file

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

View file

@ -21,7 +21,6 @@ import (
"crypto/sha256"
"errors"
"fmt"
"slices"
"strconv"
"time"
@ -43,19 +42,16 @@ import (
)
var (
_ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DeleteChatHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.PollHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.MessageRequestAcceptingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DisappearTimerChangingNetworkAPI = (*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")
}
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,16 +150,15 @@ 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)
}
@ -200,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{
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[:],
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
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),
TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
},
}))
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil {
return nil, err
}
@ -222,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{
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[:],
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
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),
TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
},
}))
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil {
return err
}
@ -246,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{
Timestamp: proto.Uint64(ts),
Delete: &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
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
}
@ -311,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
}
@ -378,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)
}
@ -397,24 +385,22 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.
}, nil)
}
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
var targetIntent bridgev2.MatrixAPI
var targetSignalID libsignalgo.ServiceID
var err error
if msg.Portal.RoomType == database.RoomTypeDM {
//TODO: this probably needs to revert some changes and clean up the portal on leaves
switch msg.Type {
case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm")
return false, fmt.Errorf("cannot invite additional user to dm")
default:
return nil, nil
return false, nil
}
}
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
if err != nil {
return nil, fmt.Errorf("failed to parse target signal id: %w", err)
return false, fmt.Errorf("failed to parse target signal id: %w", err)
}
switch target := msg.Target.(type) {
case *bridgev2.Ghost:
@ -424,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)).
@ -449,7 +435,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
switch msg.Type {
case bridgev2.AcceptInvite:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept invite for non-ACI service ID")
return false, fmt.Errorf("can't accept invite for non-ACI service ID")
}
gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{
ACI: targetSignalID.UUID,
@ -458,7 +444,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
case bridgev2.Leave, bridgev2.Kick:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't kick non-ACI service ID")
return false, fmt.Errorf("can't kick non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.Invite:
@ -494,7 +480,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
// }}
case bridgev2.AcceptKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept knock from non-ACI service ID")
return false, fmt.Errorf("can't accept knock from non-ACI service ID")
}
gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{
ACI: targetSignalID.UUID,
@ -502,7 +488,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
}}
case bridgev2.RetractKnock, bridgev2.RejectKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't reject knock from non-ACI service ID")
return false, fmt.Errorf("can't reject knock from non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft:
@ -513,39 +499,40 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
switch msg.Type {
case bridgev2.BanJoined:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
return false, fmt.Errorf("can't ban joined non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanInvited:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
case bridgev2.BanKnocked:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
return false, fmt.Errorf("can't ban knocked non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
}
case bridgev2.Unban:
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID}
default:
return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To)
log.Debug().Msg("unsupported membership change")
return false, nil
}
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil || groupID == "" {
return nil, err
return false, err
}
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil {
return nil, err
return false, err
}
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
if err != nil {
return nil, err
return false, err
}
}
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return nil, nil
return true, nil
}
func plToRole(pl int) signalmeow.GroupMemberRole {
@ -679,11 +666,13 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
})
} else {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
res := s.Client.SendMessage(ctx, userID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
Timestamp: ptr.Ptr(ts),
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
}))
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
}
@ -691,222 +680,3 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
return true, nil
}
}
func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
if msg.Content.FromMessageRequest {
// TODO block and delete support?
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE)
if err != nil {
return fmt.Errorf("failed to send message request delete sync: %w", err)
}
}
// Build ConversationIdentifier based on portal type
var conversationID *signalpb.ConversationIdentifier
if groupID == "" {
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadServiceIdBinary{
ThreadServiceIdBinary: userID.Bytes(),
},
}
} else {
gid, err := groupID.Bytes()
if err != nil {
return fmt.Errorf("failed to parse group ID: %w", err)
}
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadGroupId{
ThreadGroupId: gid[:],
},
}
}
// Retrieve most recent messages from the portal
var mostRecentMessages []*signalpb.AddressableMessage
dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(
ctx,
msg.Portal.PortalKey,
time.Now().Add(-30*24*time.Hour), // Last 30 days
time.Now(),
)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get recent messages for conversation delete")
} else if len(dbMessages) > 0 {
// Limit to the 5 most recent messages overall
limit := 5
startIdx := 0
if len(dbMessages) > limit {
startIdx = len(dbMessages) - limit
}
// Create AddressableMessage for most recent messages
for _, dbMsg := range dbMessages[startIdx:] {
senderACI, timestamp, err := signalid.ParseMessageID(dbMsg.ID)
if err != nil {
continue
}
mostRecentMessages = append(mostRecentMessages, &signalpb.AddressableMessage{
Author: &signalpb.AddressableMessage_AuthorServiceIdBinary{
AuthorServiceIdBinary: senderACI[:],
},
SentTimestamp: proto.Uint64(timestamp),
})
}
}
recipientID := s.Client.Store.ACIServiceID()
// Send DeleteForMe sync message to self
result := s.Client.SendMessage(ctx, recipientID, signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
Content: &signalpb.SyncMessage_DeleteForMe_{
DeleteForMe: &signalpb.SyncMessage_DeleteForMe{
ConversationDeletes: []*signalpb.SyncMessage_DeleteForMe_ConversationDelete{{
Conversation: conversationID,
MostRecentMessages: mostRecentMessages,
IsFullDelete: proto.Bool(true),
}},
},
},
}))
zerolog.Ctx(ctx).Debug().
Str("portal_id", string(msg.Portal.ID)).
Int("recent_messages_count", len(mostRecentMessages)).
Msg("Sent conversation deletion to Signal")
if !result.WasSuccessful {
return fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID)
}
return nil
}
func (s *SignalClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
optionNames := make([]string, len(msg.Content.PollStart.Answers))
optionIDs := make([]string, len(msg.Content.PollStart.Answers))
for i, option := range msg.Content.PollStart.Answers {
optionNames[i] = option.Text
optionIDs[i] = option.ID
}
converted := &signalpb.DataMessage{
PollCreate: &signalpb.DataMessage_PollCreate{
Question: ptr.Ptr(msg.Content.PollStart.Question.Text),
AllowMultiple: ptr.Ptr(msg.Content.PollStart.MaxSelections != 1),
Options: optionNames,
},
RequiredProtocolVersion: ptr.Ptr(uint32(signalpb.DataMessage_POLLS)),
}
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{
MatrixPollOptionIDs: optionIDs,
})
}
func (s *SignalClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
senderACI, msgTS, err := signalid.ParseMessageID(msg.VoteTo.ID)
if err != nil {
return nil, err
}
mxOptions := msg.VoteTo.Metadata.(*signalid.MessageMetadata).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)
}
}
converted := &signalpb.DataMessage{
PollVote: &signalpb.DataMessage_PollVote{
TargetAuthorAciBinary: senderACI[:],
TargetSentTimestamp: &msgTS,
OptionIndexes: optionIndexes,
VoteCount: proto.Uint32(1), // TODO
},
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

@ -26,8 +26,6 @@ import (
"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"
@ -37,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"
@ -55,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:
@ -103,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"
}
@ -175,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
@ -184,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
@ -293,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 ""
}
@ -316,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) {
@ -339,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() {
@ -364,7 +352,7 @@ func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Porta
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
}
// 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])
editPart.Part.EditCount++
@ -426,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)
@ -472,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))
})
@ -483,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
}
@ -504,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().
@ -549,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).
@ -655,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)
@ -713,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
@ -741,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

@ -52,6 +52,8 @@ type QRLogin struct {
cancelChan context.CancelFunc
ProvChan chan signalmeow.ProvisioningResponse
newQRCount int
ProvData *store.DeviceData
}
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
@ -75,7 +77,7 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
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(
@ -110,6 +112,14 @@ 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 {
@ -122,7 +132,15 @@ 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.
@ -140,13 +158,26 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
}
}
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{
@ -159,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

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

View file

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

View file

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

@ -26,91 +26,34 @@ import (
type ErrorCode int
func (e ErrorCode) Error() string {
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
}
const (
ErrorCodeUnknownError ErrorCode = 1
ErrorCodeInvalidState ErrorCode = 2
ErrorCodeInternalError ErrorCode = 3
ErrorCodeNullParameter ErrorCode = 4
ErrorCodeInvalidArgument ErrorCode = 5
ErrorCodeInvalidType ErrorCode = 6
ErrorCodeInvalidUtf8String ErrorCode = 7
ErrorCodeCancelled ErrorCode = 8
ErrorCodeProtobufError ErrorCode = 10
ErrorCodeLegacyCiphertextVersion ErrorCode = 21
ErrorCodeUnknownCiphertextVersion ErrorCode = 22
ErrorCodeUnrecognizedMessageVersion ErrorCode = 23
ErrorCodeInvalidMessage ErrorCode = 30
ErrorCodeSealedSenderSelfSend ErrorCode = 31
ErrorCodeInvalidKey ErrorCode = 40
ErrorCodeInvalidSignature ErrorCode = 41
ErrorCodeInvalidAttestationData ErrorCode = 42
ErrorCodeFingerprintVersionMismatch ErrorCode = 51
ErrorCodeFingerprintParsingError ErrorCode = 52
ErrorCodeUntrustedIdentity ErrorCode = 60
ErrorCodeInvalidKeyIdentifier ErrorCode = 70
ErrorCodeSessionNotFound ErrorCode = 80
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
ErrorCodeUnknownError ErrorCode = 1
ErrorCodeInvalidState ErrorCode = 2
ErrorCodeInternalError ErrorCode = 3
ErrorCodeNullParameter ErrorCode = 4
ErrorCodeInvalidArgument ErrorCode = 5
ErrorCodeInvalidType ErrorCode = 6
ErrorCodeInvalidUtf8String ErrorCode = 7
ErrorCodeProtobufError ErrorCode = 10
ErrorCodeLegacyCiphertextVersion ErrorCode = 21
ErrorCodeUnknownCiphertextVersion ErrorCode = 22
ErrorCodeUnrecognizedMessageVersion ErrorCode = 23
ErrorCodeInvalidMessage ErrorCode = 30
ErrorCodeSealedSenderSelfSend ErrorCode = 31
ErrorCodeInvalidKey ErrorCode = 40
ErrorCodeInvalidSignature ErrorCode = 41
ErrorCodeInvalidAttestationData ErrorCode = 42
ErrorCodeFingerprintVersionMismatch ErrorCode = 51
ErrorCodeFingerprintParsingError ErrorCode = 52
ErrorCodeUntrustedIdentity ErrorCode = 60
ErrorCodeInvalidKeyIdentifier ErrorCode = 70
ErrorCodeSessionNotFound ErrorCode = 80
ErrorCodeInvalidRegistrationId ErrorCode = 81
ErrorCodeInvalidSession ErrorCode = 82
ErrorCodeInvalidSenderKeySession ErrorCode = 83
ErrorCodeDuplicatedMessage ErrorCode = 90
ErrorCodeCallbackError ErrorCode = 100
ErrorCodeVerificationFailure ErrorCode = 110
)
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

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

@ -76,10 +76,6 @@ func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
}
}
func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
return DeriveGroupSecretParamsFromMasterKey(gmk)
}
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
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)))

View file

@ -1,215 +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"
"unsafe"
)
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),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(groupSecretParams)),
)
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)*17)
for i, member := range groupMembers {
copy(concatenatedMembers[i*17:(i+1)*17], member.FixedBytes()[:])
}
signalFfiError := C.signal_group_send_endorsements_response_receive_and_combine_with_service_ids(
&out,
BytesToBuffer(gser),
BytesToBuffer(concatenatedMembers),
localUser.CFixedBytes(),
C.uint64_t(time.Now().Unix()),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(params)),
C.SignalConstPointerServerPublicParams{spp},
)
runtime.KeepAlive(gser)
runtime.KeepAlive(concatenatedMembers)
runtime.KeepAlive(params)
runtime.KeepAlive(spp)
if signalFfiError != nil {
return nil, nil, wrapError(signalFfiError)
}
endorsements := CopySignalBytestringArray[GroupSendEndorsement](out)
memberEndorsements := make(map[ServiceID]GroupSendEndorsement, len(groupMembers))
for i, member := range groupMembers {
if len(endorsements) > i && len(endorsements[i]) > 0 {
memberEndorsements[member] = endorsements[i]
}
}
combined, err := GroupSendEndorsementCombine(endorsements...)
if err != nil {
return nil, memberEndorsements, err
}
return combined, memberEndorsements, nil
}

View file

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

View file

@ -20,12 +20,14 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *keyp);
typedef const SignalProtocolAddress const_address;
typedef const SignalPublicKey const_public_key;
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPrivateKey **keyp);
extern int signal_get_local_registration_id_callback(void *store_ctx, uint32_t *idp);
extern int signal_save_identity_key_callback(void *store_ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
extern int signal_get_identity_key_callback(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address);
extern int signal_is_trusted_identity_callback(void *store_ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
extern int signal_save_identity_key_callback(void *store_ctx, const_address *address, const_public_key *public_key);
extern int signal_get_identity_key_callback(void *store_ctx, SignalPublicKey **public_keyp, const_address *address);
extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
*/
import "C"
import (
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
}
//export signal_get_identity_key_pair_callback
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp *C.SignalPairOfMutPointerPrivateKeyMutPointerPublicKey) C.int {
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp **C.SignalPrivateKey) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
key, err := store.GetIdentityKeyPair(ctx)
if err != nil {
return err
}
if key == nil {
keyp.first.raw = nil
keyp.second.raw = nil
return nil
*keyp = nil
} else {
clone, err := key.privateKey.Clone()
if err != nil {
return err
}
clone.CancelFinalizer()
*keyp = clone.ptr
}
privClone, err := key.privateKey.Clone()
if err != nil {
return err
}
pubClone, err := key.publicKey.Clone()
if err != nil {
return err
}
privClone.CancelFinalizer()
pubClone.CancelFinalizer()
keyp.first.raw = privClone.ptr
keyp.second.raw = pubClone.ptr
return err
})
}
@ -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),
ctx: wrapStore(ctx, store),
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 bbc16886cae2feab1cd1fe271ccc651e8860ce96
Subproject commit c02859f57552477680fb7928c3c3426193040313

View file

@ -29,8 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
#define SignalBackupId_LEN 16
#define SignalCallLinkSecretParams_ROOT_KEY_MAX_BYTES_FOR_SHO 16
#define SignalNUM_AUTH_CRED_ATTRIBUTES 3
#define SignalNUM_PROFILE_KEY_CRED_ATTRIBUTES 4
@ -235,7 +233,6 @@ typedef enum {
SignalErrorCodeChatServiceInactive = 149,
SignalErrorCodeRequestTimedOut = 150,
SignalErrorCodeRateLimitChallenge = 151,
SignalErrorCodePossibleCaptiveNetwork = 152,
SignalErrorCodeSvrDataMissing = 160,
SignalErrorCodeSvrRestoreFailed = 161,
SignalErrorCodeSvrRotationMachineTooManySteps = 162,
@ -258,10 +255,6 @@ typedef enum {
SignalErrorCodeRegistrationLock = 201,
SignalErrorCodeKeyTransparencyError = 210,
SignalErrorCodeKeyTransparencyVerificationFailed = 211,
SignalErrorCodeRequestUnauthorized = 220,
SignalErrorCodeMismatchedDevices = 221,
SignalErrorCodeServiceIdNotFound = 222,
SignalErrorCodeUploadTooLarge = 223,
} SignalErrorCode;
enum SignalSvr2CredentialsResult {
@ -346,8 +339,6 @@ typedef struct SignalPrivateKey SignalPrivateKey;
*/
typedef struct SignalProtocolAddress SignalProtocolAddress;
typedef struct SignalProvisioningChatConnection SignalProvisioningChatConnection;
typedef struct SignalPublicKey SignalPublicKey;
typedef struct SignalRegisterAccountRequest SignalRegisterAccountRequest;
@ -512,59 +503,15 @@ typedef struct {
const SignalAuthenticatedChatConnection *raw;
} SignalConstPointerAuthenticatedChatConnection;
/**
* A type alias to be used with [`OwnedBufferOf`], so that `OwnedBufferOf<c_char>` and
* `OwnedBufferOf<*const c_char>` get distinct names.
*/
typedef const char *SignalCStringPtr;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
SignalCStringPtr *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfCStringPtr;
typedef struct {
uint32_t cdn;
SignalCStringPtr key;
SignalOwnedBufferOfCStringPtr header_keys;
SignalOwnedBufferOfCStringPtr header_values;
SignalCStringPtr signed_upload_url;
} SignalFfiUploadForm;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalFfiUploadForm *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseFfiUploadForm;
typedef SignalConnectionInfo SignalChatConnectionInfo;
typedef struct {
SignalChatConnectionInfo *raw;
} SignalMutPointerChatConnectionInfo;
typedef struct {
SignalServerMessageAck *raw;
} SignalMutPointerServerMessageAck;
typedef void (*SignalReceivedIncomingMessage)(void *ctx, SignalOwnedBuffer envelope, uint64_t timestamp_millis, SignalServerMessageAck *cleanup);
typedef int (*SignalFfiChatListenerReceivedIncomingMessage)(void *ctx, SignalOwnedBuffer envelope, uint64_t timestamp, SignalMutPointerServerMessageAck ack);
typedef int (*SignalFfiChatListenerReceivedQueueEmpty)(void *ctx);
typedef void (*SignalReceivedQueueEmpty)(void *ctx);
/**
* A representation of a array allocated on the Rust heap for use in C code.
@ -584,25 +531,57 @@ typedef struct {
typedef SignalBytestringArray SignalStringArray;
typedef int (*SignalFfiChatListenerReceivedAlerts)(void *ctx, SignalStringArray alerts);
typedef void (*SignalReceivedAlerts)(void *ctx, SignalStringArray alerts);
typedef int (*SignalFfiChatListenerConnectionInterrupted)(void *ctx, SignalFfiError *disconnect_cause);
typedef void (*SignalConnectionInterrupted)(void *ctx, SignalFfiError *error);
typedef void (*SignalFfiChatListenerDestroy)(void *ctx);
typedef void (*SignalDestroyChatListener)(void *ctx);
/**
* Callbacks for [`ChatListener`].
*
* Callbacks will be serialized (i.e. two calls will not come in at the same time), but may not
* always happen on the same thread. Calls should be responded to promptly to avoid blocking later
* messages.
*
* # Safety
*
* This type contains raw pointers. Code that constructs an instance of this type must ensure
* memory safety assuming that
* - the callback function pointer fields are called with `ctx` as an argument;
* - the `destroy` function pointer field is called with `ctx` as an argument;
* - no function pointer fields are called after `destroy` is called.
*/
typedef struct {
void *ctx;
SignalFfiChatListenerReceivedIncomingMessage received_incoming_message;
SignalFfiChatListenerReceivedQueueEmpty received_queue_empty;
SignalFfiChatListenerReceivedAlerts received_alerts;
SignalFfiChatListenerConnectionInterrupted connection_interrupted;
SignalFfiChatListenerDestroy destroy;
SignalReceivedIncomingMessage received_incoming_message;
SignalReceivedQueueEmpty received_queue_empty;
SignalReceivedAlerts received_alerts;
SignalConnectionInterrupted connection_interrupted;
SignalDestroyChatListener destroy;
} SignalFfiChatListenerStruct;
typedef struct {
const SignalFfiChatListenerStruct *raw;
} SignalConstPointerFfiChatListenerStruct;
/**
* A type alias to be used with [`OwnedBufferOf`], so that `OwnedBufferOf<c_char>` and
* `OwnedBufferOf<*const c_char>` get distinct names.
*/
typedef const char *SignalCStringPtr;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
SignalCStringPtr *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfCStringPtr;
typedef struct {
uint16_t status;
const char *message;
@ -636,27 +615,6 @@ typedef struct {
*/
typedef uint8_t SignalServiceIdFixedWidthBinaryBytes[17];
typedef struct {
const uint32_t *base;
size_t length;
} SignalBorrowedSliceOfu32;
typedef struct {
const SignalCiphertextMessage *raw;
} SignalConstPointerCiphertextMessage;
typedef struct {
const SignalConstPointerCiphertextMessage *base;
size_t length;
} SignalBorrowedSliceOfConstPointerCiphertextMessage;
/**
* A wrapper type for raw UUIDs, because C treats arrays specially in argument position.
*/
typedef struct {
uint8_t bytes[16];
} SignalUuid;
typedef struct {
SignalPrivateKey *raw;
} SignalMutPointerPrivateKey;
@ -768,6 +726,10 @@ typedef struct {
const SignalPlaintextContent *raw;
} SignalConstPointerPlaintextContent;
typedef struct {
const SignalCiphertextMessage *raw;
} SignalConstPointerCiphertextMessage;
typedef struct {
SignalConnectionInfo *raw;
} SignalMutPointerConnectionInfo;
@ -792,52 +754,49 @@ typedef struct {
SignalSessionRecord *raw;
} SignalMutPointerSessionRecord;
typedef int (*SignalFfiSessionStoreLoadSession)(void *ctx, SignalMutPointerSessionRecord *out, SignalMutPointerProtocolAddress address);
typedef int (*SignalLoadSession)(void *store_ctx, SignalMutPointerSessionRecord *recordp, SignalConstPointerProtocolAddress address);
typedef int (*SignalFfiSessionStoreStoreSession)(void *ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
typedef struct {
const SignalSessionRecord *raw;
} SignalConstPointerSessionRecord;
typedef void (*SignalFfiSessionStoreDestroy)(void *ctx);
typedef int (*SignalStoreSession)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerSessionRecord record);
typedef struct {
void *ctx;
SignalFfiSessionStoreLoadSession load_session;
SignalFfiSessionStoreStoreSession store_session;
SignalFfiSessionStoreDestroy destroy;
SignalLoadSession load_session;
SignalStoreSession store_session;
} SignalSessionStore;
typedef struct {
const SignalSessionStore *raw;
} SignalConstPointerFfiSessionStoreStruct;
typedef int (*SignalGetIdentityKeyPair)(void *store_ctx, SignalMutPointerPrivateKey *keyp);
typedef int (*SignalGetLocalRegistrationId)(void *store_ctx, uint32_t *idp);
typedef struct {
const SignalPublicKey *raw;
} SignalConstPointerPublicKey;
typedef int (*SignalSaveIdentityKey)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key);
typedef struct {
SignalPublicKey *raw;
} SignalMutPointerPublicKey;
typedef struct {
SignalMutPointerPrivateKey first;
SignalMutPointerPublicKey second;
} SignalPairOfMutPointerPrivateKeyMutPointerPublicKey;
typedef int (*SignalGetIdentityKey)(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalConstPointerProtocolAddress address);
typedef int (*SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair)(void *ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *out);
typedef int (*SignalFfiIdentityKeyStoreGetLocalRegistrationId)(void *ctx, uint32_t *out);
typedef int (*SignalFfiIdentityKeyStoreGetIdentityKey)(void *ctx, SignalMutPointerPublicKey *out, SignalMutPointerProtocolAddress address);
typedef int (*SignalFfiIdentityKeyStoreSaveIdentityKey)(void *ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
typedef int (*SignalFfiIdentityKeyStoreIsTrustedIdentity)(void *ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
typedef void (*SignalFfiIdentityKeyStoreDestroy)(void *ctx);
typedef int (*SignalIsTrustedIdentity)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key, unsigned int direction);
typedef struct {
void *ctx;
SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair get_local_identity_key_pair;
SignalFfiIdentityKeyStoreGetLocalRegistrationId get_local_registration_id;
SignalFfiIdentityKeyStoreGetIdentityKey get_identity_key;
SignalFfiIdentityKeyStoreSaveIdentityKey save_identity_key;
SignalFfiIdentityKeyStoreIsTrustedIdentity is_trusted_identity;
SignalFfiIdentityKeyStoreDestroy destroy;
SignalGetIdentityKeyPair get_identity_key_pair;
SignalGetLocalRegistrationId get_local_registration_id;
SignalSaveIdentityKey save_identity;
SignalGetIdentityKey get_identity;
SignalIsTrustedIdentity is_trusted_identity;
} SignalIdentityKeyStore;
typedef struct {
@ -852,20 +811,21 @@ typedef struct {
SignalPreKeyRecord *raw;
} SignalMutPointerPreKeyRecord;
typedef int (*SignalFfiPreKeyStoreLoadPreKey)(void *ctx, SignalMutPointerPreKeyRecord *out, uint32_t id);
typedef int (*SignalLoadPreKey)(void *store_ctx, SignalMutPointerPreKeyRecord *recordp, uint32_t id);
typedef int (*SignalFfiPreKeyStoreStorePreKey)(void *ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
typedef struct {
const SignalPreKeyRecord *raw;
} SignalConstPointerPreKeyRecord;
typedef int (*SignalFfiPreKeyStoreRemovePreKey)(void *ctx, uint32_t id);
typedef int (*SignalStorePreKey)(void *store_ctx, uint32_t id, SignalConstPointerPreKeyRecord record);
typedef void (*SignalFfiPreKeyStoreDestroy)(void *ctx);
typedef int (*SignalRemovePreKey)(void *store_ctx, uint32_t id);
typedef struct {
void *ctx;
SignalFfiPreKeyStoreLoadPreKey load_pre_key;
SignalFfiPreKeyStoreStorePreKey store_pre_key;
SignalFfiPreKeyStoreRemovePreKey remove_pre_key;
SignalFfiPreKeyStoreDestroy destroy;
SignalLoadPreKey load_pre_key;
SignalStorePreKey store_pre_key;
SignalRemovePreKey remove_pre_key;
} SignalPreKeyStore;
typedef struct {
@ -876,17 +836,18 @@ typedef struct {
SignalSignedPreKeyRecord *raw;
} SignalMutPointerSignedPreKeyRecord;
typedef int (*SignalFfiSignedPreKeyStoreLoadSignedPreKey)(void *ctx, SignalMutPointerSignedPreKeyRecord *out, uint32_t id);
typedef int (*SignalLoadSignedPreKey)(void *store_ctx, SignalMutPointerSignedPreKeyRecord *recordp, uint32_t id);
typedef int (*SignalFfiSignedPreKeyStoreStoreSignedPreKey)(void *ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
typedef struct {
const SignalSignedPreKeyRecord *raw;
} SignalConstPointerSignedPreKeyRecord;
typedef void (*SignalFfiSignedPreKeyStoreDestroy)(void *ctx);
typedef int (*SignalStoreSignedPreKey)(void *store_ctx, uint32_t id, SignalConstPointerSignedPreKeyRecord record);
typedef struct {
void *ctx;
SignalFfiSignedPreKeyStoreLoadSignedPreKey load_signed_pre_key;
SignalFfiSignedPreKeyStoreStoreSignedPreKey store_signed_pre_key;
SignalFfiSignedPreKeyStoreDestroy destroy;
SignalLoadSignedPreKey load_signed_pre_key;
SignalStoreSignedPreKey store_signed_pre_key;
} SignalSignedPreKeyStore;
typedef struct {
@ -897,20 +858,21 @@ typedef struct {
SignalKyberPreKeyRecord *raw;
} SignalMutPointerKyberPreKeyRecord;
typedef int (*SignalFfiKyberPreKeyStoreLoadKyberPreKey)(void *ctx, SignalMutPointerKyberPreKeyRecord *out, uint32_t id);
typedef int (*SignalLoadKyberPreKey)(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id);
typedef int (*SignalFfiKyberPreKeyStoreStoreKyberPreKey)(void *ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
typedef struct {
const SignalKyberPreKeyRecord *raw;
} SignalConstPointerKyberPreKeyRecord;
typedef int (*SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed)(void *ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
typedef int (*SignalStoreKyberPreKey)(void *store_ctx, uint32_t id, SignalConstPointerKyberPreKeyRecord record);
typedef void (*SignalFfiKyberPreKeyStoreDestroy)(void *ctx);
typedef int (*SignalMarkKyberPreKeyUsed)(void *store_ctx, uint32_t id, uint32_t signed_prekey_id, SignalConstPointerPublicKey base_key);
typedef struct {
void *ctx;
SignalFfiKyberPreKeyStoreLoadKyberPreKey load_kyber_pre_key;
SignalFfiKyberPreKeyStoreStoreKyberPreKey store_kyber_pre_key;
SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed mark_kyber_pre_key_used;
SignalFfiKyberPreKeyStoreDestroy destroy;
SignalLoadKyberPreKey load_kyber_pre_key;
SignalStoreKyberPreKey store_kyber_pre_key;
SignalMarkKyberPreKeyUsed mark_kyber_pre_key_used;
} SignalKyberPreKeyStore;
typedef struct {
@ -935,45 +897,11 @@ typedef struct {
uint32_t second;
} SignalPairOfc_charu32;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
uint32_t *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfu32;
typedef struct {
SignalServiceIdFixedWidthBinaryBytes account;
SignalOwnedBufferOfu32 missing_devices;
SignalOwnedBufferOfu32 extra_devices;
SignalOwnedBufferOfu32 stale_devices;
} SignalFfiMismatchedDevicesError;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
SignalFfiMismatchedDevicesError *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfFfiMismatchedDevicesError;
typedef struct {
const char *first;
SignalOwnedBuffer second;
} SignalPairOfc_charOwnedBufferOfc_uchar;
typedef struct {
SignalPairOfc_charOwnedBufferOfc_uchar first;
int64_t second;
} SignalPairOfPairOfc_charOwnedBufferOfc_uchari64;
typedef struct {
const char *first;
bool second;
@ -987,10 +915,6 @@ typedef struct {
const SignalFingerprint *raw;
} SignalConstPointerFingerprint;
typedef struct {
const SignalPublicKey *raw;
} SignalConstPointerPublicKey;
typedef struct {
/**
* The badge ID.
@ -1017,47 +941,22 @@ typedef struct {
size_t length;
} SignalOwnedBufferOfFfiRegisterResponseBadge;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
SignalServiceIdFixedWidthBinaryBytes *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfServiceIdFixedWidthBinaryBytes;
typedef struct {
SignalPreKeyBundle *raw;
} SignalMutPointerPreKeyBundle;
/**
* A representation of a array allocated on the Rust heap for use in C code.
*/
typedef struct {
SignalMutPointerPreKeyBundle *base;
/**
* The number of elements in the buffer (not necessarily the number of bytes).
*/
size_t length;
} SignalOwnedBufferOfMutPointerPreKeyBundle;
typedef struct {
SignalSenderKeyRecord *raw;
} SignalMutPointerSenderKeyRecord;
typedef int (*SignalFfiSenderKeyStoreLoadSenderKey)(void *ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id);
typedef int (*SignalLoadSenderKey)(void *store_ctx, SignalMutPointerSenderKeyRecord*, SignalConstPointerProtocolAddress, const uint8_t (*distribution_id)[16]);
typedef int (*SignalFfiSenderKeyStoreStoreSenderKey)(void *ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
typedef struct {
const SignalSenderKeyRecord *raw;
} SignalConstPointerSenderKeyRecord;
typedef void (*SignalFfiSenderKeyStoreDestroy)(void *ctx);
typedef int (*SignalStoreSenderKey)(void *store_ctx, SignalConstPointerProtocolAddress, const uint8_t (*distribution_id)[16], SignalConstPointerSenderKeyRecord);
typedef struct {
void *ctx;
SignalFfiSenderKeyStoreLoadSenderKey load_sender_key;
SignalFfiSenderKeyStoreStoreSenderKey store_sender_key;
SignalFfiSenderKeyStoreDestroy destroy;
SignalLoadSenderKey load_sender_key;
SignalStoreSenderKey store_sender_key;
} SignalSenderKeyStore;
typedef struct {
@ -1102,23 +1001,15 @@ typedef struct {
SignalIncrementalMac *raw;
} SignalMutPointerIncrementalMac;
typedef int (*SignalFfiLoggerLog)(void *ctx, SignalLogLevel level, const char *file, uint32_t line, const char *message);
typedef void (*SignalLogCallback)(void *ctx, SignalLogLevel level, const char *file, uint32_t line, const char *message);
typedef int (*SignalFfiLoggerFlush)(void *ctx);
typedef void (*SignalFfiLoggerDestroy)(void *ctx);
typedef void (*SignalLogFlushCallback)(void *ctx);
typedef struct {
void *ctx;
SignalFfiLoggerLog log;
SignalFfiLoggerFlush flush;
SignalFfiLoggerDestroy destroy;
} SignalFfiLoggerStruct;
typedef struct {
SignalOwnedBuffer first;
SignalOwnedBuffer second;
} SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
SignalLogCallback log;
SignalLogFlushCallback flush;
} SignalFfiLogger;
/**
* A C callback used to report the results of Rust futures.
@ -1130,10 +1021,10 @@ typedef struct {
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *result, const void *context);
void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
} SignalCPromiseOwnedBufferOfc_uchar;
typedef struct {
const SignalUnauthenticatedChatConnection *raw;
@ -1171,10 +1062,6 @@ typedef struct {
SignalKyberSecretKey *raw;
} SignalMutPointerKyberSecretKey;
typedef struct {
const SignalKyberPreKeyRecord *raw;
} SignalConstPointerKyberPreKeyRecord;
typedef struct {
const SignalKyberPublicKey *raw;
} SignalConstPointerKyberPublicKey;
@ -1203,17 +1090,14 @@ typedef struct {
const SignalMessageBackupValidationOutcome *raw;
} SignalConstPointerMessageBackupValidationOutcome;
typedef int (*SignalFfiInputStreamRead)(void *ctx, size_t *out, SignalBorrowedMutableBuffer buf);
typedef int (*SignalRead)(void *ctx, uint8_t *buf, size_t buf_len, size_t *amount_read);
typedef int (*SignalFfiInputStreamSkip)(void *ctx, uint64_t amount);
typedef void (*SignalFfiInputStreamDestroy)(void *ctx);
typedef int (*SignalSkip)(void *ctx, uint64_t amount);
typedef struct {
void *ctx;
SignalFfiInputStreamRead read;
SignalFfiInputStreamSkip skip;
SignalFfiInputStreamDestroy destroy;
SignalRead read;
SignalSkip skip;
} SignalInputStream;
typedef struct {
@ -1245,12 +1129,12 @@ typedef struct {
} SignalMutPointerPlaintextContent;
typedef struct {
const SignalPreKeyBundle *raw;
} SignalConstPointerPreKeyBundle;
SignalPreKeyBundle *raw;
} SignalMutPointerPreKeyBundle;
typedef struct {
const SignalPreKeyRecord *raw;
} SignalConstPointerPreKeyRecord;
const SignalPreKeyBundle *raw;
} SignalConstPointerPreKeyBundle;
typedef struct {
SignalPreKeySignalMessage *raw;
@ -1260,49 +1144,6 @@ typedef struct {
const SignalSenderKeyDistributionMessage *raw;
} SignalConstPointerSenderKeyDistributionMessage;
typedef struct {
SignalProvisioningChatConnection *raw;
} SignalMutPointerProvisioningChatConnection;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalMutPointerProvisioningChatConnection *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseMutPointerProvisioningChatConnection;
typedef struct {
const SignalProvisioningChatConnection *raw;
} SignalConstPointerProvisioningChatConnection;
typedef int (*SignalFfiProvisioningListenerReceivedAddress)(void *ctx, const char *address, SignalMutPointerServerMessageAck send_ack);
typedef int (*SignalFfiProvisioningListenerReceivedEnvelope)(void *ctx, SignalOwnedBuffer envelope, SignalMutPointerServerMessageAck send_ack);
typedef int (*SignalFfiProvisioningListenerConnectionInterrupted)(void *ctx, SignalFfiError *disconnect_cause);
typedef void (*SignalFfiProvisioningListenerDestroy)(void *ctx);
typedef struct {
void *ctx;
SignalFfiProvisioningListenerReceivedAddress received_address;
SignalFfiProvisioningListenerReceivedEnvelope received_envelope;
SignalFfiProvisioningListenerConnectionInterrupted connection_interrupted;
SignalFfiProvisioningListenerDestroy destroy;
} SignalFfiProvisioningListenerStruct;
typedef struct {
const SignalFfiProvisioningListenerStruct *raw;
} SignalConstPointerFfiProvisioningListenerStruct;
typedef struct {
SignalRegisterAccountRequest *raw;
} SignalMutPointerRegisterAccountRequest;
@ -1326,10 +1167,7 @@ typedef struct {
const SignalRegisterAccountResponse *raw;
} SignalConstPointerRegisterAccountResponse;
typedef struct {
bool present;
uint8_t bytes[16];
} SignalOptionalUuid;
typedef uint8_t SignalOptionalUuid[17];
typedef SignalAccountAttributes SignalRegistrationAccountAttributes;
@ -1444,10 +1282,6 @@ typedef struct {
size_t length;
} SignalBorrowedSliceOfConstPointerProtocolAddress;
typedef struct {
const SignalSessionRecord *raw;
} SignalConstPointerSessionRecord;
typedef struct {
const SignalConstPointerSessionRecord *base;
size_t length;
@ -1527,8 +1361,8 @@ typedef struct {
} SignalConstPointerSenderKeyMessage;
typedef struct {
const SignalSenderKeyRecord *raw;
} SignalConstPointerSenderKeyRecord;
SignalServerMessageAck *raw;
} SignalMutPointerServerMessageAck;
typedef struct {
const SignalServerMessageAck *raw;
@ -1546,10 +1380,6 @@ typedef struct {
const SignalSgxClientState *raw;
} SignalConstPointerSgxClientState;
typedef struct {
const SignalSignedPreKeyRecord *raw;
} SignalConstPointerSignedPreKeyRecord;
typedef struct {
SignalTokioAsyncContext *raw;
} SignalMutPointerTokioAsyncContext;
@ -1573,26 +1403,6 @@ typedef struct {
SignalCancellationId cancellation_id;
} SignalCPromiseMutPointerUnauthenticatedChatConnection;
typedef struct {
SignalMutPointerPublicKey identity_key;
SignalOwnedBufferOfMutPointerPreKeyBundle pre_key_bundles;
} SignalFfiPreKeysResponse;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalFfiPreKeysResponse *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseFfiPreKeysResponse;
/**
* A C callback used to report the results of Rust futures.
*
@ -1608,42 +1418,6 @@ typedef struct {
SignalCancellationId cancellation_id;
} SignalCPromiseOptionalUuid;
typedef struct {
bool present;
const char *first;
uint8_t second[32];
} SignalOptionalPairOfc_charu832;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalOptionalPairOfc_charu832 *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseOptionalPairOfc_charu832;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalOwnedBufferOfServiceIdFixedWidthBinaryBytes *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseOwnedBufferOfServiceIdFixedWidthBinaryBytes;
typedef struct {
SignalValidatingMac *raw;
} SignalMutPointerValidatingMac;
@ -1658,8 +1432,6 @@ typedef uint8_t SignalRandomnessBytes[SignalRANDOMNESS_LEN];
typedef uint8_t SignalUnidentifiedAccessKey[SignalACCESS_KEY_LEN];
SignalFfiError *signal_account_entropy_pool_derive_backup_key(uint8_t (*out)[SignalBACKUP_KEY_LEN], const char *account_entropy);
SignalFfiError *signal_account_entropy_pool_derive_svr_key(uint8_t (*out)[SignalSVR_KEY_LEN], const char *account_entropy);
@ -1726,8 +1498,6 @@ SignalFfiError *signal_authenticated_chat_connection_destroy(SignalMutPointerAut
SignalFfiError *signal_authenticated_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat);
SignalFfiError *signal_authenticated_chat_connection_get_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, uint64_t upload_length);
SignalFfiError *signal_authenticated_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerAuthenticatedChatConnection chat);
SignalFfiError *signal_authenticated_chat_connection_init_listener(SignalConstPointerAuthenticatedChatConnection chat, SignalConstPointerFfiChatListenerStruct listener);
@ -1736,10 +1506,6 @@ SignalFfiError *signal_authenticated_chat_connection_preconnect(SignalCPromisebo
SignalFfiError *signal_authenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
SignalFfiError *signal_authenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool online_only, bool is_urgent);
SignalFfiError *signal_authenticated_chat_connection_send_sync_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool is_urgent);
SignalFfiError *signal_backup_auth_credential_check_valid_contents(SignalBorrowedBuffer params_bytes);
SignalFfiError *signal_backup_auth_credential_get_backup_id(uint8_t (*out)[16], SignalBorrowedBuffer credential_bytes);
@ -1760,7 +1526,7 @@ SignalFfiError *signal_backup_auth_credential_request_context_check_valid_conten
SignalFfiError *signal_backup_auth_credential_request_context_get_request(SignalOwnedBuffer *out, SignalBorrowedBuffer context_bytes);
SignalFfiError *signal_backup_auth_credential_request_context_new(SignalOwnedBuffer *out, const uint8_t (*backup_key)[32], SignalUuid uuid);
SignalFfiError *signal_backup_auth_credential_request_context_new(SignalOwnedBuffer *out, const uint8_t (*backup_key)[32], const uint8_t (*uuid)[16]);
SignalFfiError *signal_backup_auth_credential_request_context_receive_response(SignalOwnedBuffer *out, SignalBorrowedBuffer context_bytes, SignalBorrowedBuffer response_bytes, uint64_t expected_redemption_time, SignalBorrowedBuffer params_bytes);
@ -1900,9 +1666,9 @@ SignalFfiError *signal_create_call_link_credential_request_issue_deterministic(S
SignalFfiError *signal_create_call_link_credential_response_check_valid_contents(SignalBorrowedBuffer response_bytes);
SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
SignalFfiError *signal_decryption_error_message_clone(SignalMutPointerDecryptionErrorMessage *new_obj, SignalConstPointerDecryptionErrorMessage obj);
@ -1928,7 +1694,7 @@ SignalFfiError *signal_device_transfer_generate_private_key(SignalOwnedBuffer *o
SignalFfiError *signal_device_transfer_generate_private_key_with_format(SignalOwnedBuffer *out, uint8_t key_format);
SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
void signal_error_free(SignalFfiError *err);
@ -1938,11 +1704,9 @@ SignalFfiError *signal_error_get_invalid_protocol_address(SignalPairOfc_charu32
SignalFfiError *signal_error_get_message(const char **out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_mismatched_device_errors(SignalOwnedBufferOfFfiMismatchedDevicesError *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_our_fingerprint_version(uint32_t *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_rate_limit_challenge(SignalPairOfPairOfc_charOwnedBufferOfc_uchari64 *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_rate_limit_challenge(SignalPairOfc_charOwnedBufferOfc_uchar *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_registration_error_not_deliverable(SignalPairOfc_charbool *out, SignalUnwindSafeArgSignalFfiError err);
@ -1958,7 +1722,7 @@ uint32_t signal_error_get_type(const SignalFfiError *err);
SignalFfiError *signal_error_get_unknown_fields(SignalStringArray *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_uuid(SignalUuid *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_uuid(uint8_t (*out)[16], SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_expiring_profile_key_credential_check_valid_contents(SignalBorrowedBuffer buffer);
@ -1982,23 +1746,12 @@ void signal_free_buffer(const unsigned char *buf, size_t buf_len);
void signal_free_bytestring_array(SignalBytestringArray array);
void signal_free_list_of_mismatched_device_errors(SignalOwnedBufferOfFfiMismatchedDevicesError buffer);
void signal_free_list_of_register_response_badges(SignalOwnedBufferOfFfiRegisterResponseBadge buffer);
void signal_free_list_of_service_ids(SignalOwnedBufferOfServiceIdFixedWidthBinaryBytes buffer);
void signal_free_list_of_strings(SignalOwnedBufferOfCStringPtr buffer);
void signal_free_lookup_response_entry_list(SignalOwnedBufferOfFfiCdsiLookupResponseEntry buffer);
/**
* This frees a buffer of PreKeyBundle pointers, and _does not_ free the
* pointers within the buffer. This _only_ frees the buffer containing
* the pointers.
*/
void signal_free_outer_buffer_list_of_prekey_bundles(SignalOwnedBufferOfMutPointerPreKeyBundle buffer);
void signal_free_string(const char *buf);
SignalFfiError *signal_generic_server_public_params_check_valid_contents(SignalBorrowedBuffer params_bytes);
@ -2011,7 +1764,7 @@ SignalFfiError *signal_generic_server_secret_params_get_public_params(SignalOwne
SignalFfiError *signal_group_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerProtocolAddress sender, SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_group_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalConstPointerProtocolAddress sender, SignalUuid distribution_id, SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_group_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalConstPointerProtocolAddress sender, const uint8_t (*distribution_id)[16], SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_group_master_key_check_valid_contents(SignalBorrowedBuffer buffer);
@ -2117,14 +1870,18 @@ SignalFfiError *signal_incremental_mac_initialize(SignalMutPointerIncrementalMac
SignalFfiError *signal_incremental_mac_update(SignalOwnedBuffer *out, SignalMutPointerIncrementalMac mac, SignalBorrowedBuffer bytes, uint32_t offset, uint32_t length);
bool signal_init_logger(SignalLogLevel max_level, SignalFfiLoggerStruct logger);
bool signal_init_logger(SignalLogLevel max_level, SignalFfiLogger logger);
SignalFfiError *signal_key_transparency_aci_search_key(SignalOwnedBuffer *out, const SignalServiceIdFixedWidthBinaryBytes *aci);
SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
SignalFfiError *signal_key_transparency_distinguished(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head);
SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164);
SignalFfiError *signal_key_transparency_monitor(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head, bool is_self_monitor);
SignalFfiError *signal_key_transparency_search(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head);
SignalFfiError *signal_key_transparency_username_hash_search_key(SignalOwnedBuffer *out, SignalBorrowedBuffer hash);
SignalFfiError *signal_kyber_key_pair_clone(SignalMutPointerKyberKeyPair *new_obj, SignalConstPointerKyberKeyPair obj);
@ -2355,7 +2112,7 @@ SignalFfiError *signal_privatekey_serialize(SignalOwnedBuffer *out, SignalConstP
SignalFfiError *signal_privatekey_sign(SignalOwnedBuffer *out, SignalConstPointerPrivateKey key, SignalBorrowedBuffer message);
SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
SignalFfiError *signal_process_sender_key_distribution_message(SignalConstPointerProtocolAddress sender, SignalConstPointerSenderKeyDistributionMessage sender_key_distribution_message, SignalConstPointerFfiSenderKeyStoreStruct store);
@ -2383,18 +2140,10 @@ SignalFfiError *signal_profile_key_get_commitment(unsigned char (*out)[SignalPRO
SignalFfiError *signal_profile_key_get_profile_key_version(uint8_t (*out)[SignalPROFILE_KEY_VERSION_ENCODED_LEN], const unsigned char (*profile_key)[SignalPROFILE_KEY_LEN], const SignalServiceIdFixedWidthBinaryBytes *user_id);
SignalFfiError *signal_provisioning_chat_connection_connect(SignalCPromiseMutPointerProvisioningChatConnection *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerConnectionManager connection_manager);
SignalFfiError *signal_provisioning_chat_connection_destroy(SignalMutPointerProvisioningChatConnection p);
SignalFfiError *signal_provisioning_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerProvisioningChatConnection chat);
SignalFfiError *signal_provisioning_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerProvisioningChatConnection chat);
SignalFfiError *signal_provisioning_chat_connection_init_listener(SignalConstPointerProvisioningChatConnection chat, SignalConstPointerFfiProvisioningListenerStruct listener);
SignalFfiError *signal_publickey_clone(SignalMutPointerPublicKey *new_obj, SignalConstPointerPublicKey obj);
SignalFfiError *signal_publickey_compare(int32_t *out, SignalConstPointerPublicKey key1, SignalConstPointerPublicKey key2);
SignalFfiError *signal_publickey_deserialize(SignalMutPointerPublicKey *out, SignalBorrowedBuffer data);
SignalFfiError *signal_publickey_destroy(SignalMutPointerPublicKey p);
@ -2567,7 +2316,7 @@ SignalFfiError *signal_sender_certificate_validate(bool *out, SignalConstPointer
SignalFfiError *signal_sender_key_distribution_message_clone(SignalMutPointerSenderKeyDistributionMessage *new_obj, SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_create(SignalMutPointerSenderKeyDistributionMessage *out, SignalConstPointerProtocolAddress sender, SignalUuid distribution_id, SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_sender_key_distribution_message_create(SignalMutPointerSenderKeyDistributionMessage *out, SignalConstPointerProtocolAddress sender, const uint8_t (*distribution_id)[16], SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_sender_key_distribution_message_deserialize(SignalMutPointerSenderKeyDistributionMessage *out, SignalBorrowedBuffer data);
@ -2577,13 +2326,13 @@ SignalFfiError *signal_sender_key_distribution_message_get_chain_id(uint32_t *ou
SignalFfiError *signal_sender_key_distribution_message_get_chain_key(SignalOwnedBuffer *out, SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_distribution_id(SignalUuid *out, SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_distribution_id(uint8_t (*out)[16], SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_iteration(uint32_t *out, SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_signature_key(SignalMutPointerPublicKey *out, SignalConstPointerSenderKeyDistributionMessage m);
SignalFfiError *signal_sender_key_distribution_message_new(SignalMutPointerSenderKeyDistributionMessage *out, uint8_t message_version, SignalUuid distribution_id, uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer chainkey, SignalConstPointerPublicKey pk);
SignalFfiError *signal_sender_key_distribution_message_new(SignalMutPointerSenderKeyDistributionMessage *out, uint8_t message_version, const uint8_t (*distribution_id)[16], uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer chainkey, SignalConstPointerPublicKey pk);
SignalFfiError *signal_sender_key_distribution_message_serialize(SignalOwnedBuffer *out, SignalConstPointerSenderKeyDistributionMessage obj);
@ -2597,11 +2346,11 @@ SignalFfiError *signal_sender_key_message_get_chain_id(uint32_t *out, SignalCons
SignalFfiError *signal_sender_key_message_get_cipher_text(SignalOwnedBuffer *out, SignalConstPointerSenderKeyMessage obj);
SignalFfiError *signal_sender_key_message_get_distribution_id(SignalUuid *out, SignalConstPointerSenderKeyMessage obj);
SignalFfiError *signal_sender_key_message_get_distribution_id(uint8_t (*out)[16], SignalConstPointerSenderKeyMessage obj);
SignalFfiError *signal_sender_key_message_get_iteration(uint32_t *out, SignalConstPointerSenderKeyMessage obj);
SignalFfiError *signal_sender_key_message_new(SignalMutPointerSenderKeyMessage *out, uint8_t message_version, SignalUuid distribution_id, uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer ciphertext, SignalConstPointerPrivateKey pk);
SignalFfiError *signal_sender_key_message_new(SignalMutPointerSenderKeyMessage *out, uint8_t message_version, const uint8_t (*distribution_id)[16], uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer ciphertext, SignalConstPointerPrivateKey pk);
SignalFfiError *signal_sender_key_message_serialize(SignalOwnedBuffer *out, SignalConstPointerSenderKeyMessage obj);
@ -2755,38 +2504,20 @@ SignalFfiError *signal_tokio_async_context_destroy(SignalMutPointerTokioAsyncCon
SignalFfiError *signal_tokio_async_context_new(SignalMutPointerTokioAsyncContext *out);
SignalFfiError *signal_unauthenticated_chat_connection_account_exists(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *account);
SignalFfiError *signal_unauthenticated_chat_connection_backup_get_media_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer credential, SignalBorrowedBuffer server_keys, SignalConstPointerPrivateKey signing_key, uint64_t upload_size, int64_t rng);
SignalFfiError *signal_unauthenticated_chat_connection_backup_get_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer credential, SignalBorrowedBuffer server_keys, SignalConstPointerPrivateKey signing_key, uint64_t upload_size, int64_t rng);
SignalFfiError *signal_unauthenticated_chat_connection_connect(SignalCPromiseMutPointerUnauthenticatedChatConnection *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerConnectionManager connection_manager, SignalBorrowedBytestringArray languages);
SignalFfiError *signal_unauthenticated_chat_connection_destroy(SignalMutPointerUnauthenticatedChatConnection p);
SignalFfiError *signal_unauthenticated_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat);
SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_access_key_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const uint8_t (*auth)[16], const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_group_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer auth, const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_unrestricted_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
SignalFfiError *signal_unauthenticated_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerUnauthenticatedChatConnection chat);
SignalFfiError *signal_unauthenticated_chat_connection_init_listener(SignalConstPointerUnauthenticatedChatConnection chat, SignalConstPointerFfiChatListenerStruct listener);
SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_hash(SignalCPromiseOptionalUuid *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer hash);
SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_link(SignalCPromiseOptionalPairOfc_charu832 *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalUuid uuid, SignalBorrowedBuffer entropy);
SignalFfiError *signal_unauthenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
SignalFfiError *signal_unauthenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfBuffers contents, uint8_t auth_kind, SignalOptionalBorrowedSliceOfc_uchar auth_buffer, bool online_only, bool is_urgent);
SignalFfiError *signal_unauthenticated_chat_connection_send_multi_recipient_message(SignalCPromiseOwnedBufferOfServiceIdFixedWidthBinaryBytes *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer payload, uint64_t timestamp, SignalBorrowedBuffer auth, bool online_only, bool is_urgent);
SignalFfiError *signal_unidentified_sender_message_content_deserialize(SignalMutPointerUnidentifiedSenderMessageContent *out, SignalBorrowedBuffer data);
SignalFfiError *signal_unidentified_sender_message_content_destroy(SignalMutPointerUnidentifiedSenderMessageContent p);

View file

@ -21,7 +21,6 @@ package libsignalgo
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 (
@ -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),
)

View file

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

View file

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

View file

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

View file

@ -23,7 +23,6 @@ package libsignalgo
*/
import "C"
import (
"encoding/base64"
"errors"
"runtime"
"unsafe"
@ -55,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[:])
}
@ -66,23 +69,6 @@ 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 < C.SignalACCESS_KEY_LEN; 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) {
c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))

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

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

@ -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,19 +118,6 @@ 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)
@ -161,18 +151,6 @@ func ServiceIDFromString(val string) (ServiceID, error) {
}
}
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) {
if len(bytes) == 16 {
return NewACIServiceID(uuid.UUID(bytes)), nil
} else if len(bytes) == 17 {
return ServiceID{
Type: ServiceIDType(bytes[0]),
UUID: uuid.UUID(bytes[1:]),
}, nil
}
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected 16 or 17)", len(bytes))
}
func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
var id ServiceID
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))

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,7 +1,7 @@
#!/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 cbindgen
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

View file

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

View file

@ -44,6 +44,7 @@ func (mc *MessageConverter) ToSignal(
portal *bridgev2.Portal,
evt *event.Event,
content *event.MessageEventContent,
timestamp uint64,
relaybotFormatted bool,
replyTo *database.Message,
) (*signalpb.DataMessage, error) {
@ -54,15 +55,16 @@ func (mc *MessageConverter) ToSignal(
}
dm := &signalpb.DataMessage{
Preview: mc.convertURLPreviewToSignal(ctx, content),
Timestamp: &timestamp,
Preview: mc.convertURLPreviewToSignal(ctx, content),
}
if replyTo != nil {
authorACI, messageID, err := signalid.ParseMessageID(replyTo.ID)
if err == nil {
dm.Quote = &signalpb.DataMessage_Quote{
Id: proto.Uint64(messageID),
AuthorAciBinary: authorACI[:],
Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
Id: proto.Uint64(messageID),
AuthorAci: proto.String(authorACI.String()),
Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
}
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
dm.Quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1)
@ -110,24 +112,21 @@ func (mc *MessageConverter) ToSignal(
return nil, fmt.Errorf("failed to convert sticker: %w", err)
}
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 {
emoji = proto.String(variationselector.Remove(content.Body))
}
dm.Sticker = &signalpb.DataMessage_Sticker{
// Signal iOS validates that pack id/key are of the correct length.
// Android is fine with any non-nil values (like a zero-length byte string).
PackId: make([]byte, 16),
PackKey: make([]byte, 32),
StickerId: proto.Uint32(0),
Emoji: emoji,
}
var emoji *string
// TODO check for single grapheme cluster?
if len([]rune(content.Body)) == 1 {
emoji = proto.String(variationselector.Remove(content.Body))
}
dm.Sticker = &signalpb.DataMessage_Sticker{
// Signal iOS validates that pack id/key are of the correct length.
// Android is fine with any non-nil values (like a zero-length byte string).
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,10 +22,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -54,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)
@ -78,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,
@ -113,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, 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)
@ -177,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()),
@ -343,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 {
@ -461,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
}
@ -508,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
}
@ -521,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()),
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 {
@ -560,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
}
@ -569,7 +568,7 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
content.Info.Blurhash = att.GetBlurHash()
content.Info.AnoaBlurhash = att.GetBlurHash()
}
switch strings.Split(content.Info.MimeType, "/")[0] {
switch strings.Split(mimeType, "/")[0] {
case "image":
content.MsgType = event.MsgImage
case "video":
@ -591,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,
@ -600,186 +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, 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
}
mxOptionIDs := pollMessage.Metadata.(*signalid.MessageMetadata).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

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

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

View file

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

View file

@ -31,11 +31,8 @@ import (
"math"
"mime/multipart"
"net/http"
"os"
"github.com/rs/zerolog"
"go.mau.fi/util/fallocate"
"go.mau.fi/util/pkcs7"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
@ -62,54 +59,26 @@ var ErrInvalidMACForAttachment = errors.New("invalid MAC for attachment")
var ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment")
var ErrAttachmentNotFound = errors.New("attachment not found on server")
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte, into *os.File) ([]byte, error) {
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte) ([]byte, error) {
digest := a.GetDigest()
plaintextDigest := false
if digest == nil && plaintextHash != nil {
digest = plaintextHash
plaintextDigest = true
}
return DownloadAttachment(
ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize(), into,
)
return DownloadAttachment(ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize())
}
func DownloadAttachment(
ctx context.Context,
cdnID uint64,
cdnKey string,
cdnNumber uint32,
key, digest []byte,
plaintextDigest bool,
size uint32,
into *os.File,
) ([]byte, error) {
resp, err := web.GetAttachment(ctx, getAttachmentPath(cdnID, cdnKey), cdnNumber)
func DownloadAttachment(ctx context.Context, cdnID uint64, cdnKey string, cdnNumber uint32, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
path := getAttachmentPath(cdnID, cdnKey)
resp, err := web.GetAttachment(ctx, path, cdnNumber, nil)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
bodyReader := resp.Body
defer bodyReader.Close()
var body []byte
var downloadedSize int64
if resp.StatusCode > 400 {
body, err = io.ReadAll(io.LimitReader(resp.Body, 4096))
} else if into == nil {
if resp.ContentLength > 0 {
body = make([]byte, resp.ContentLength)
_, err = io.ReadFull(resp.Body, body)
} else {
body, err = io.ReadAll(http.MaxBytesReader(nil, resp.Body, max(int64(size), 32*1024)*2))
}
} else {
err = fallocate.Fallocate(into, int(resp.ContentLength))
if err != nil {
return nil, fmt.Errorf("failed to pre-allocate file for attachment: %w", err)
}
downloadedSize, err = io.Copy(into, resp.Body)
}
body, err := io.ReadAll(bodyReader)
if err != nil {
return nil, err
}
@ -125,27 +94,12 @@ func DownloadAttachment(
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
if into != nil {
if _, err = into.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek attachment file after downloading: %w", err)
}
return nil, decryptAttachmentFile(into, downloadedSize, key, digest, plaintextDigest, size)
}
return decryptAttachment(body, key, digest, plaintextDigest, size)
}
const MACLength = 32
const IVLength = 16
func macAndAESDecrypt(body, key []byte) ([]byte, error) {
l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
return aesDecrypt(key[:MACLength], body[:l])
}
func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
if !plaintextDigest {
hash := sha256.Sum256(body)
@ -153,7 +107,12 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return nil, ErrInvalidDigestForAttachment
}
}
decrypted, err := macAndAESDecrypt(body, key)
l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
decrypted, err := aesDecrypt(key[:MACLength], body[:l])
if err != nil {
return nil, err
}
@ -170,59 +129,6 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return decrypted, nil
}
func decryptAttachmentFile(file *os.File, downloadedSize int64, key, digest []byte, plaintextDigest bool, size uint32) error {
if !plaintextDigest {
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return ErrInvalidDigestForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing: %w", err)
}
}
mac := make([]byte, MACLength)
n, err := file.ReadAt(mac, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to read MAC from attachment file: %w", err)
} else if n != MACLength {
return fmt.Errorf("unexpected MAC length read from attachment file: %d", n)
}
hasher := hmac.New(sha256.New, key[MACLength:])
_, err = io.CopyN(hasher, file, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to hash attachment file for MAC verification: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), mac) {
return ErrInvalidMACForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after verifying mac: %w", err)
}
decryptedSize, err := aesDecryptFile(key[:MACLength], file, downloadedSize-MACLength)
if err != nil {
return err
} else if decryptedSize < int64(size) {
return fmt.Errorf("decrypted attachment length %d < expected %d", decryptedSize, size)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after decrypting: %w", err)
}
err = file.Truncate(int64(size))
if err != nil {
return fmt.Errorf("failed to truncate attachment file to expected size: %w", err)
}
if plaintextDigest {
hasher = sha256.New()
if _, err = io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash decrypted attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return fmt.Errorf("%w (plaintext hash)", ErrInvalidDigestForAttachment)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing plaintext: %w", err)
}
}
return nil
}
type attachmentV4UploadAttributes struct {
Cdn uint32 `json:"cdn"`
Key string `json:"key"`
@ -245,14 +151,6 @@ func extend(data []byte, paddedLen int) []byte {
}
}
func macAndAESEncrypt(keys, plaintext []byte) ([]byte, error) {
encrypted, err := aesEncrypt(keys[:32], plaintext)
if err != nil {
return nil, err
}
return appendMAC(keys[32:], encrypted), nil
}
func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
keys := random.Bytes(64) // combined AES and MAC keys
@ -268,20 +166,23 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
}
body = extend(body, paddedLen)
encryptedWithMAC, err := macAndAESEncrypt(keys, body)
encrypted, err := aesEncrypt(keys[:32], body)
if err != nil {
return nil, err
}
encryptedWithMAC := appendMAC(keys[32:], encrypted)
// Get upload attributes from Signal server
attributesPath := "/v4/attachments/form/upload"
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil)
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, attributesPath, opts)
if err != nil {
log.Err(err).Msg("Failed to request upload attributes")
return nil, fmt.Errorf("failed to request upload attributes: %w", err)
}
var uploadAttributes attachmentV4UploadAttributes
err = web.DecodeWSResponseBody(ctx, &uploadAttributes, resp)
err = web.DecodeHTTPResponseBody(ctx, &uploadAttributes, resp)
if err != nil {
log.Err(err).Msg("Failed to decode upload attributes")
return nil, fmt.Errorf("failed to decode upload attributes: %w", err)
@ -291,7 +192,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC)
} else {
log.Trace().Msg("Using legacy upload")
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC)
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC, username, password)
}
if err != nil {
log.Err(err).Msg("Failed to upload attachment")
@ -317,17 +218,17 @@ func (cli *Client) uploadAttachmentLegacy(
ctx context.Context,
uploadAttributes attachmentV4UploadAttributes,
encryptedWithMAC []byte,
username string,
password string,
) error {
username, password := cli.Store.BasicAuthCreds()
// Allocate attachment on CDN
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation,
ContentType: web.ContentTypeOctetStream,
Headers: uploadAttributes.Headers,
Username: &username,
Password: &password,
})
web.CloseBody(resp)
if err != nil {
return fmt.Errorf("failed to send allocate request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -335,14 +236,13 @@ func (cli *Client) uploadAttachmentLegacy(
}
// Upload attachment to CDN
resp, err = web.SendHTTPRequest(ctx, "", http.MethodPut, "", &web.HTTPReqOpt{
resp, err = web.SendHTTPRequest(ctx, http.MethodPut, "", &web.HTTPReqOpt{
OverrideURL: resp.Header.Get("Location"),
Body: encryptedWithMAC,
ContentType: web.ContentTypeOctetStream,
Username: &username,
Password: &password,
})
web.CloseBody(resp)
if err != nil {
return fmt.Errorf("failed to send upload request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -360,13 +260,12 @@ func (cli *Client) uploadAttachmentTUS(
uploadAttributes.Headers["Upload-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC))
uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key))
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation,
Body: encryptedWithMAC,
ContentType: web.ContentTypeOffsetOctetStream,
Headers: uploadAttributes.Headers,
})
web.CloseBody(resp)
// TODO actually support resuming on error
if err != nil {
return fmt.Errorf("failed to send upload request: %w", err)
@ -381,17 +280,12 @@ func (cli *Client) uploadAttachmentTUS(
return nil
}
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier, groupMasterKey types.SerializedGroupMasterKey) (string, error) {
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier) (string, error) {
log := zerolog.Ctx(ctx)
if groupMasterKey == "" {
var err error
groupMasterKey, err = cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
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)
}
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
}
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,16 +35,14 @@ type Client struct {
Store *store.Device
Log zerolog.Logger
senderCertificateWithE164 *libsignalgo.SenderCertificate
senderCertificateNoE164 *libsignalgo.SenderCertificate
senderCertificateCache sync.Mutex
sendCache *exsync.RingBuffer[sendCacheKey, *signalpb.Content]
GroupCache *GroupCache
ProfileCache *ProfileCache
LastContactRequestTime time.Time
SyncContactsOnConnect bool
SenderCertificateWithE164 *libsignalgo.SenderCertificate
SenderCertificateNoE164 *libsignalgo.SenderCertificate
GroupCredentials *GroupCredentials
GroupCache *GroupCache
ProfileCache *ProfileCache
GroupCallCache *map[string]bool
LastContactRequestTime time.Time
SyncContactsOnConnect bool
encryptionLock sync.Mutex
@ -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

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

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

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

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,139 +1,92 @@
/*
* Copyright 2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto3";
package signal;
option java_package = "org.signal.storageservice.storage.protos.groups";
option java_outer_classname = "GroupProtos";
option java_package = "org.signal.storageservice.protos.groups";
option java_multiple_files = true;
message AvatarUploadAttributes {
string key = 1;
string key = 1;
string credential = 2;
string acl = 3;
string algorithm = 4;
string date = 5;
string policy = 6;
string signature = 7;
string acl = 3;
string algorithm = 4;
string date = 5;
string policy = 6;
string signature = 7;
}
// Stored data
message Member {
enum Role {
UNKNOWN = 0;
DEFAULT = 1;
UNKNOWN = 0;
DEFAULT = 1;
ADMINISTRATOR = 2;
}
bytes userId = 1;
Role role = 2;
bytes profileKey = 3;
bytes presentation = 4;
uint32 joinedAtVersion = 5;
bytes labelEmoji = 6; // decrypts to a UTF-8 string
bytes labelString = 7; // decrypts to a UTF-8 string
bytes userId = 1;
Role role = 2;
bytes profileKey = 3;
bytes presentation = 4; // Only set when sending to server
uint32 joinedAtRevision = 5;
}
message MemberPendingProfileKey {
Member member = 1;
bytes addedByUserId = 2;
uint64 timestamp = 3; // ms since epoch
message PendingMember {
Member member = 1;
bytes addedByUserId = 2;
uint64 timestamp = 3;
}
message MemberPendingAdminApproval {
bytes userId = 1;
bytes profileKey = 2;
bytes presentation = 3;
uint64 timestamp = 4; // ms since epoch
message RequestingMember {
bytes userId = 1;
bytes profileKey = 2;
bytes presentation = 3; // Only set when sending to server
uint64 timestamp = 4;
}
message MemberBanned {
bytes userId = 1;
uint64 timestamp = 2; // ms since epoch
message BannedMember {
bytes userId = 1;
uint64 timestamp = 2;
}
message AccessControl {
enum AccessRequired {
UNKNOWN = 0;
ANY = 1;
MEMBER = 2;
UNKNOWN = 0;
ANY = 1;
MEMBER = 2;
ADMINISTRATOR = 3;
UNSATISFIABLE = 4;
}
AccessRequired attributes = 1;
AccessRequired members = 2;
AccessRequired attributes = 1;
AccessRequired members = 2;
AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
}
message Group {
bytes publicKey = 1;
bytes title = 2;
bytes description = 11;
// The URL for this group's avatar. The content at this URL can be
// decrypted/deserialized into a `GroupAttributeBlob`.
string avatarUrl = 3;
bytes disappearingMessagesTimer = 4;
AccessControl accessControl = 5;
uint32 version = 6;
repeated Member members = 7;
repeated MemberPendingProfileKey membersPendingProfileKey = 8;
repeated MemberPendingAdminApproval membersPendingAdminApproval = 9;
bytes inviteLinkPassword = 10;
bool announcements_only = 12;
repeated MemberBanned members_banned = 13;
bool terminated = 14;
// next: 15
bytes publicKey = 1;
bytes title = 2;
string avatar = 3;
bytes disappearingMessagesTimer = 4;
AccessControl accessControl = 5;
uint32 revision = 6;
repeated Member members = 7;
repeated PendingMember pendingMembers = 8;
repeated RequestingMember requestingMembers = 9;
bytes inviteLinkPassword = 10;
bytes description = 11;
bool announcementsOnly = 12;
repeated BannedMember bannedMembers = 13;
}
message GroupAttributeBlob {
oneof content {
string title = 1;
bytes avatar = 2;
uint32 disappearingMessagesDuration = 3;
string descriptionText = 4;
}
}
message GroupInviteLink {
message GroupInviteLinkContentsV1 {
bytes groupMasterKey = 1;
bytes inviteLinkPassword = 2;
}
oneof contents {
GroupInviteLinkContentsV1 contentsV1 = 1;
}
}
message GroupJoinInfo {
bytes publicKey = 1;
bytes title = 2;
bytes description = 8;
string avatar = 3;
uint32 memberCount = 4;
AccessControl.AccessRequired addFromInviteLink = 5;
uint32 version = 6;
bool pendingAdminApproval = 7;
// bool pendingAdminApprovalFull = 9;
// next: 10
}
// Deltas
message GroupChange {
message Actions {
message AddMemberAction {
Member added = 1;
bool joinFromInviteLink = 2;
Member added = 1;
bool joinFromInviteLink = 2;
}
message DeleteMemberAction {
@ -141,61 +94,55 @@ message GroupChange {
}
message ModifyMemberRoleAction {
bytes userId = 1;
Member.Role role = 2;
}
message ModifyMemberLabelAction {
bytes userId = 1;
bytes labelEmoji = 2; // decrypts to a UTF-8 string
bytes labelString = 3; // decrypts to a UTF-8 string
bytes userId = 1;
Member.Role role = 2;
}
message ModifyMemberProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
bytes presentation = 1; // Only set when sending to server
bytes user_id = 2; // Only set when receiving from server
bytes profile_key = 3; // Only set when receiving from server
}
message AddMemberPendingProfileKeyAction {
MemberPendingProfileKey added = 1;
message AddPendingMemberAction {
PendingMember added = 1;
}
message DeleteMemberPendingProfileKeyAction {
message DeletePendingMemberAction {
bytes deletedUserId = 1;
}
message PromoteMemberPendingProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
message PromotePendingMemberAction {
bytes presentation = 1; // Only set when sending to server
bytes user_id = 2; // Only set when receiving from server
bytes profile_key = 3; // Only set when receiving from server
}
message PromoteMemberPendingPniAciProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes pni = 3;
bytes profile_key = 4;
message PromotePendingPniAciMemberProfileKeyAction {
bytes presentation = 1; // Only set when sending to server
bytes userId = 2; // Only set when receiving from server
bytes pni = 3; // Only set when receiving from server
bytes profileKey = 4; // Only set when receiving from server
}
message AddMemberPendingAdminApprovalAction {
MemberPendingAdminApproval added = 1;
message AddRequestingMemberAction {
RequestingMember added = 1;
}
message DeleteMemberPendingAdminApprovalAction {
message DeleteRequestingMemberAction {
bytes deletedUserId = 1;
}
message PromoteMemberPendingAdminApprovalAction {
bytes userId = 1;
Member.Role role = 2;
message PromoteRequestingMemberAction {
bytes userId = 1;
Member.Role role = 2;
}
message AddMemberBannedAction {
MemberBanned added = 1;
message AddBannedMemberAction {
BannedMember added = 1;
}
message DeleteMemberBannedAction {
message DeleteBannedMemberAction {
bytes deletedUserId = 1;
}
@ -211,7 +158,7 @@ message GroupChange {
string avatar = 1;
}
message ModifyDisappearingMessageTimerAction {
message ModifyDisappearingMessagesTimerAction {
bytes timer = 1;
}
@ -227,83 +174,97 @@ message GroupChange {
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
}
message ModifyMemberLabelAccessControlAction {
AccessControl.AccessRequired memberLabelAccess = 1;
}
message ModifyInviteLinkPasswordAction {
bytes inviteLinkPassword = 1;
}
message ModifyAnnouncementsOnlyAction {
bool announcements_only = 1;
bool announcementsOnly = 1;
}
message TerminateGroupAction {}
bytes sourceUserId = 1;
// clients should not provide this value; the server will provide it in the response buffer to ensure the signature is binding to a particular group
// if clients set it during a request the server will respond with 400.
bytes group_id = 25;
uint32 version = 2;
repeated AddMemberAction addMembers = 3;
repeated DeleteMemberAction deleteMembers = 4;
repeated ModifyMemberRoleAction modifyMemberRoles = 5;
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
repeated AddMemberPendingProfileKeyAction addMembersPendingProfileKey = 7;
repeated DeleteMemberPendingProfileKeyAction deleteMembersPendingProfileKey = 8;
repeated PromoteMemberPendingProfileKeyAction promoteMembersPendingProfileKey = 9;
ModifyTitleAction modifyTitle = 10;
ModifyAvatarAction modifyAvatar = 11;
ModifyDisappearingMessageTimerAction modifyDisappearingMessageTimer = 12;
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
ModifyMembersAccessControlAction modifyMemberAccess = 14;
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15; // change epoch = 1
repeated AddMemberPendingAdminApprovalAction addMembersPendingAdminApproval = 16; // change epoch = 1
repeated DeleteMemberPendingAdminApprovalAction deleteMembersPendingAdminApproval = 17; // change epoch = 1
repeated PromoteMemberPendingAdminApprovalAction promoteMembersPendingAdminApproval = 18; // change epoch = 1
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; // change epoch = 1
ModifyDescriptionAction modifyDescription = 20; // change epoch = 2
ModifyAnnouncementsOnlyAction modify_announcements_only = 21; // change epoch = 3
repeated AddMemberBannedAction add_members_banned = 22; // change epoch = 4
repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4
repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5
repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6;
ModifyMemberLabelAccessControlAction modifyMemberLabelAccess = 27; // change epoch = 6
TerminateGroupAction terminate_group = 28; // change epoch = 7
// next: 29
bytes sourceServiceId = 1;
bytes groupId = 25; // Only set when receiving from server
uint32 revision = 2;
repeated AddMemberAction addMembers = 3;
repeated DeleteMemberAction deleteMembers = 4;
repeated ModifyMemberRoleAction modifyMemberRoles = 5;
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
repeated AddPendingMemberAction addPendingMembers = 7;
repeated DeletePendingMemberAction deletePendingMembers = 8;
repeated PromotePendingMemberAction promotePendingMembers = 9;
ModifyTitleAction modifyTitle = 10;
ModifyAvatarAction modifyAvatar = 11;
ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12;
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
ModifyMembersAccessControlAction modifyMemberAccess = 14;
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15;
repeated AddRequestingMemberAction addRequestingMembers = 16;
repeated DeleteRequestingMemberAction deleteRequestingMembers = 17;
repeated PromoteRequestingMemberAction promoteRequestingMembers = 18;
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19;
ModifyDescriptionAction modifyDescription = 20;
ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21;
repeated AddBannedMemberAction addBannedMembers = 22;
repeated DeleteBannedMemberAction deleteBannedMembers = 23;
repeated PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMembers = 24;
}
bytes actions = 1;
bytes serverSignature = 2;
uint32 changeEpoch = 3;
bytes actions = 1;
bytes serverSignature = 2;
uint32 changeEpoch = 3;
}
// External credentials
message ExternalGroupCredential {
string token = 1;
}
// API responses
message GroupResponse {
Group group = 1;
bytes group_send_endorsements_response = 2;
Group group = 1;
bytes groupSendEndorsementsResponse = 2;
}
message GroupChanges {
message GroupChangeState {
GroupChange groupChange = 1;
Group groupState = 2;
Group groupState = 2;
}
repeated GroupChangeState groupChanges = 1;
bytes group_send_endorsements_response = 2;
repeated GroupChangeState groupChanges = 1;
bytes groupSendEndorsementsResponse = 2;
}
message GroupChangeResponse {
GroupChange group_change = 1;
bytes group_send_endorsements_response = 2;
GroupChange groupChange = 1;
bytes groupSendEndorsementsResponse = 2;
}
message GroupAttributeBlob {
oneof content {
string title = 1;
bytes avatar = 2;
uint32 disappearingMessagesDuration = 3;
string description = 4;
}
}
message GroupInviteLink {
message GroupInviteLinkContentsV1 {
bytes groupMasterKey = 1;
bytes inviteLinkPassword = 2;
}
oneof contents {
GroupInviteLinkContentsV1 v1Contents = 1;
}
}
message GroupJoinInfo {
bytes publicKey = 1;
bytes title = 2;
string avatar = 3;
uint32 memberCount = 4;
AccessControl.AccessRequired addFromInviteLink = 5;
uint32 revision = 6;
bool pendingAdminApproval = 7;
bytes description = 8;
}
message GroupExternalCredential {
string token = 1;
}

View file

@ -4,8 +4,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.1
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: Provisioning.proto
package signalpb
@ -199,6 +199,7 @@ type ProvisionMessage struct {
ProfileKey []byte `protobuf:"bytes,6,opt,name=profileKey" json:"profileKey,omitempty"`
ReadReceipts *bool `protobuf:"varint,7,opt,name=readReceipts" json:"readReceipts,omitempty"`
ProvisioningVersion *uint32 `protobuf:"varint,9,opt,name=provisioningVersion" json:"provisioningVersion,omitempty"`
MasterKey []byte `protobuf:"bytes,13,opt,name=masterKey" json:"masterKey,omitempty"` // Deprecated, but required by linked devices
EphemeralBackupKey []byte `protobuf:"bytes,14,opt,name=ephemeralBackupKey" json:"ephemeralBackupKey,omitempty"` // 32 bytes
AccountEntropyPool *string `protobuf:"bytes,15,opt,name=accountEntropyPool" json:"accountEntropyPool,omitempty"`
MediaRootBackupKey []byte `protobuf:"bytes,16,opt,name=mediaRootBackupKey" json:"mediaRootBackupKey,omitempty"` // 32-bytes
@ -322,6 +323,13 @@ func (x *ProvisionMessage) GetProvisioningVersion() uint32 {
return 0
}
func (x *ProvisionMessage) GetMasterKey() []byte {
if x != nil {
return x.MasterKey
}
return nil
}
func (x *ProvisionMessage) GetEphemeralBackupKey() []byte {
if x != nil {
return x.EphemeralBackupKey
@ -366,7 +374,7 @@ const file_Provisioning_proto_rawDesc = "" +
"\aaddress\x18\x01 \x01(\tR\aaddress\"E\n" +
"\x11ProvisionEnvelope\x12\x1c\n" +
"\tpublicKey\x18\x01 \x01(\fR\tpublicKey\x12\x12\n" +
"\x04body\x18\x02 \x01(\fR\x04body\"\xb4\x05\n" +
"\x04body\x18\x02 \x01(\fR\x04body\"\xcc\x05\n" +
"\x10ProvisionMessage\x122\n" +
"\x14aciIdentityKeyPublic\x18\x01 \x01(\fR\x14aciIdentityKeyPublic\x124\n" +
"\x15aciIdentityKeyPrivate\x18\x02 \x01(\fR\x15aciIdentityKeyPrivate\x122\n" +
@ -382,12 +390,13 @@ const file_Provisioning_proto_rawDesc = "" +
"profileKey\x18\x06 \x01(\fR\n" +
"profileKey\x12\"\n" +
"\freadReceipts\x18\a \x01(\bR\freadReceipts\x120\n" +
"\x13provisioningVersion\x18\t \x01(\rR\x13provisioningVersion\x12.\n" +
"\x13provisioningVersion\x18\t \x01(\rR\x13provisioningVersion\x12\x1c\n" +
"\tmasterKey\x18\r \x01(\fR\tmasterKey\x12.\n" +
"\x12ephemeralBackupKey\x18\x0e \x01(\fR\x12ephemeralBackupKey\x12.\n" +
"\x12accountEntropyPool\x18\x0f \x01(\tR\x12accountEntropyPool\x12.\n" +
"\x12mediaRootBackupKey\x18\x10 \x01(\fR\x12mediaRootBackupKey\x12\x1c\n" +
"\taciBinary\x18\x11 \x01(\fR\taciBinary\x12\x1c\n" +
"\tpniBinary\x18\x12 \x01(\fR\tpniBinaryJ\x04\b\r\x10\x0e*G\n" +
"\tpniBinary\x18\x12 \x01(\fR\tpniBinary*G\n" +
"\x13ProvisioningVersion\x12\v\n" +
"\aINITIAL\x10\x00\x12\x12\n" +
"\x0eTABLET_SUPPORT\x10\x01\x12\v\n" +

View file

@ -38,7 +38,7 @@ message ProvisionMessage {
optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 provisioningVersion = 9;
reserved /*masterKey*/ 13; // Deprecated in favor of accountEntropyPool
optional bytes masterKey = 13; // Deprecated, but required by linked devices
optional bytes ephemeralBackupKey = 14; // 32 bytes
optional string accountEntropyPool = 15;
optional bytes mediaRootBackupKey = 16; // 32-bytes

File diff suppressed because it is too large Load diff

View file

@ -13,79 +13,23 @@ option java_outer_classname = "SignalServiceProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
/**
* A double-ratchet message represents a "normal," "unsealed-sender" message
* encrypted using the Double Ratchet within an established Signal session.
* Double-ratchet messages include sender information in the plaintext
* portion of the `Envelope`.
*/
DOUBLE_RATCHET = 1; // content => (version byte | SignalMessage{Content})
CIPHERTEXT = 1; // content => (version byte | SignalMessage{Content})
reserved 2;
reserved "KEY_EXCHANGE";
/**
* A prekey message begins a new Signal session. The `content` of a prekey
* message is a superset of a double-ratchet message's `content` and
* contains the sender's identity public key and information identifying the
* pre-keys used in the message's ciphertext. Like double-ratchet messages,
* prekey messages contain sender information in the plaintext portion of
* the `Envelope`.
*/
PREKEY_MESSAGE = 3; // content => (version byte | PreKeySignalMessage{Content})
/**
* Server delivery receipts are generated by the server when
* "unsealed-sender" messages are delivered to and acknowledged by the
* destination device. Server delivery receipts identify the sender in the
* plaintext portion of the `Envelope` and have no `content`. Note that
* receipts for sealed-sender messages are generated by clients as
* `UNIDENTIFIED_SENDER` messages.
*
* Note that, with server delivery receipts, the "client timestamp" on
* the envelope refers to the timestamp of the original message (i.e. the
* message the server just delivered) and not to the time of delivery. The
* "server timestamp" refers to the time of delivery.
*/
SERVER_DELIVERY_RECEIPT = 5; // content => []
/**
* An unidentified sender message represents a message with no sender
* information in the plaintext portion of the `Envelope`. Unidentified
* sender messages always contain an additional `subtype` in their
* `content`. They may or may not be part of an existing Signal session
* (i.e. an unidentified sender message may have a "prekey message"
* subtype or may indicate an encryption error).
*/
UNIDENTIFIED_SENDER = 6; // content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
reserved 7;
reserved "SENDERKEY_MESSAGE";
/**
* A plaintext message is used solely to convey encryption error receipts
* and never contains encrypted message content. Encryption error receipts
* must be delivered in plaintext because, encryption/decryption of a prior
* message failed and there is no reason to believe that
* encryption/decryption of subsequent messages with the same key material
* would succeed.
*
* Critically, plaintext messages never have "real" message content
* generated by users. Plaintext messages include sender information.
*/
PLAINTEXT_CONTENT = 8; // content => (marker byte | Content)
// next: 9
PREKEY_BUNDLE = 3; // content => (version byte | PreKeySignalMessage{Content})
SERVER_DELIVERY_RECEIPT = 5; // legacyMessage => [] AND content => []
UNIDENTIFIED_SENDER = 6; // legacyMessage => [] AND content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
SENDERKEY_MESSAGE = 7; // legacyMessage => [] AND content => (version byte | SenderKeyMessage)
PLAINTEXT_CONTENT = 8; // legacyMessage => [] AND content => (marker byte | Content)
}
optional Type type = 1;
reserved 2; // formerly optional string sourceE164 = 2;
optional string sourceServiceId = 11;
optional uint32 sourceDeviceId = 7;
optional uint32 sourceDevice = 7;
optional string destinationServiceId = 13;
reserved 3; // formerly optional string relay = 3;
optional uint64 clientTimestamp = 5;
optional uint64 timestamp = 5;
reserved 6; // formerly optional bytes legacyMessage = 6; // Contains an encrypted DataMessage; this field could have been set historically for type 1 or 3 messages; no longer in use
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
@ -96,28 +40,21 @@ message Envelope {
optional bool story = 16; // indicates that the content is a story.
optional bytes report_spam_token = 17; // token sent when reporting spam
reserved 18; // internal server use
optional bytes sourceServiceIdBinary = 19; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
optional bytes destinationServiceIdBinary = 20; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
optional bytes serverGuidBinary = 21; // 16-byte UUID
optional bytes updatedPniBinary = 22; // 16-byte UUID
// next: 22
// next: 19
}
message Content {
oneof content {
DataMessage dataMessage = 1;
SyncMessage syncMessage = 2;
CallMessage callMessage = 3;
NullMessage nullMessage = 4;
ReceiptMessage receiptMessage = 5;
TypingMessage typingMessage = 6;
bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8;
StoryMessage storyMessage = 9;
EditMessage editMessage = 11;
}
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional bytes /* SenderKeyDistributionMessage */ senderKeyDistributionMessage = 7;
optional bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8;
optional StoryMessage storyMessage = 9;
optional PniSignatureMessage pniSignatureMessage = 10;
optional EditMessage editMessage = 11;
}
message CallMessage {
@ -240,7 +177,6 @@ message DataMessage {
enum Type {
NORMAL = 0;
GIFT_BADGE = 1;
POLL = 2;
}
message QuotedAttachment {
@ -256,7 +192,6 @@ message DataMessage {
repeated QuotedAttachment attachments = 4;
repeated BodyRange bodyRanges = 6;
optional Type type = 7;
optional bytes authorAciBinary = 8; // 16-byte UUID
}
message Contact {
@ -341,7 +276,6 @@ message DataMessage {
reserved /* targetAuthorE164 */ 3;
optional string targetAuthorAci = 4;
optional uint64 targetSentTimestamp = 5;
optional bytes targetAuthorAciBinary = 6; // 16-byte UUID
}
message Delete {
@ -355,7 +289,6 @@ message DataMessage {
message StoryContext {
optional string authorAci = 1;
optional uint64 sentTimestamp = 2;
optional bytes authorAciBinary = 3; // 16-byte UUID
}
enum ProtocolVersion {
@ -369,50 +302,13 @@ message DataMessage {
CDN_SELECTOR_ATTACHMENTS = 5;
MENTIONS = 6;
PAYMENTS = 7;
POLLS = 8;
CURRENT = 8;
CURRENT = 7;
}
message GiftBadge {
optional bytes receiptCredentialPresentation = 1;
}
message PollCreate {
optional string question = 1;
optional bool allowMultiple = 2;
repeated string options = 3;
}
message PollTerminate {
optional uint64 targetSentTimestamp = 1;
}
message PollVote {
optional bytes targetAuthorAciBinary = 1;
optional uint64 targetSentTimestamp = 2;
repeated uint32 optionIndexes = 3;
optional uint32 voteCount = 4;
}
message PinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
oneof pinDuration {
uint32 pinDurationSeconds = 3;
bool pinDurationForever = 4;
}
}
message UnpinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
}
message AdminDelete {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
reserved /*groupV1*/ 3;
@ -435,13 +331,7 @@ message DataMessage {
optional Payment payment = 20;
optional StoryContext storyContext = 21;
optional GiftBadge giftBadge = 22;
optional PollCreate pollCreate = 24;
optional PollTerminate pollTerminate = 25;
optional PollVote pollVote = 26;
optional PinMessage pinMessage = 27;
optional UnpinMessage unpinMessage = 28;
optional AdminDelete adminDelete = 29;
// NEXT ID: 30
// NEXT ID: 24
}
message NullMessage {
@ -500,12 +390,6 @@ message TextAttachment {
}
message Gradient {
// Color ordering:
// 0 degrees: bottom-to-top
// 90 degrees: left-to-right
// 180 degrees: top-to-bottom
// 270 degrees: right-to-left
optional uint32 startColor = 1; // deprecated: this field will be removed in a future release.
optional uint32 endColor = 2; // deprecated: this field will be removed in a future release.
optional uint32 angle = 3; // degrees
@ -536,7 +420,6 @@ message Verified {
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
optional bytes destinationAciBinary = 6; // 16-byte UUID
}
message SyncMessage {
@ -547,7 +430,6 @@ message SyncMessage {
optional bool unidentified = 2;
reserved /*destinationPni */ 4;
optional bytes destinationPniIdentityKey = 5; // Only set for PNI destinations
optional bytes destinationServiceIdBinary = 6; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
message StoryMessageRecipient {
@ -555,7 +437,6 @@ message SyncMessage {
repeated string distributionListIds = 2;
optional bool isAllowedToReply = 3;
reserved /*destinationPni */ 4;
optional bytes destinationServiceIdBinary = 5; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
optional string destinationE164 = 1;
@ -569,8 +450,7 @@ message SyncMessage {
repeated StoryMessageRecipient storyMessageRecipients = 9;
optional EditMessage editMessage = 10;
reserved /*destinationPni */ 11;
optional bytes destinationServiceIdBinary = 12; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
// Next ID: 13
// Next ID: 12
}
message Contacts {
@ -582,7 +462,6 @@ message SyncMessage {
repeated string numbers = 1;
repeated string acis = 3;
repeated bytes groupIds = 2;
repeated bytes acisBinary = 4; // 16-byte UUID
}
message Request {
@ -603,14 +482,12 @@ message SyncMessage {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message Viewed {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message Configuration {
@ -618,7 +495,7 @@ message SyncMessage {
optional bool unidentifiedDeliveryIndicators = 2;
optional bool typingIndicators = 3;
reserved /* linkPreviews */ 4;
reserved /* provisioningVersion */ 5;
optional uint32 provisioningVersion = 5;
optional bool linkPreviews = 6;
}
@ -637,7 +514,6 @@ message SyncMessage {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message FetchLatest {
@ -653,7 +529,7 @@ message SyncMessage {
message Keys {
reserved /* storageService */ 1;
reserved /* master */ 2;
optional bytes master = 2; // deprecated: this field will be removed in a future release.
optional string accountEntropyPool = 3;
optional bytes mediaRootBackupKey = 4;
}
@ -678,7 +554,6 @@ message SyncMessage {
optional string threadAci = 2;
optional bytes groupId = 3;
optional Type type = 4;
optional bytes threadAciBinary = 5; // 16-byte UUID
}
message OutgoingPayment {
@ -753,7 +628,7 @@ message SyncMessage {
optional bytes rootKey = 1;
optional bytes adminPasskey = 2;
optional Type type = 3; // defaults to UPDATE
reserved /*epoch*/ 4;
optional bytes epoch = 4;
}
message CallLogEvent {
@ -850,40 +725,31 @@ message SyncMessage {
}
}
oneof content {
Sent sent = 1;
Contacts contacts = 2;
Request request = 4;
Blocked blocked = 6;
Verified verified = 7;
Configuration configuration = 9;
ViewOnceOpen viewOnceOpen = 11;
FetchLatest fetchLatest = 12;
Keys keys = 13;
MessageRequestResponse messageRequestResponse = 14;
OutgoingPayment outgoingPayment = 15;
PniChangeNumber pniChangeNumber = 18;
CallEvent callEvent = 19;
CallLinkUpdate callLinkUpdate = 20;
CallLogEvent callLogEvent = 21;
DeleteForMe deleteForMe = 22;
DeviceNameChange deviceNameChange = 23;
AttachmentBackfillRequest attachmentBackfillRequest = 24;
AttachmentBackfillResponse attachmentBackfillResponse = 25;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
reserved /*groups*/ 3;
// Protobufs don't allow `repeated` fields to be inside of `oneof` so while
// the fields below are mutually exclusive with the rest of the values above
// we have to place them outside of `oneof`.
optional Request request = 4;
repeated Read read = 5;
repeated StickerPackOperation stickerPackOperation = 10;
repeated Viewed viewed = 16;
reserved /*pniIdentity*/ 17;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
optional ViewOnceOpen viewOnceOpen = 11;
optional FetchLatest fetchLatest = 12;
optional Keys keys = 13;
optional MessageRequestResponse messageRequestResponse = 14;
optional OutgoingPayment outgoingPayment = 15;
repeated Viewed viewed = 16;
reserved /*pniIdentity*/ 17;
optional PniChangeNumber pniChangeNumber = 18;
optional CallEvent callEvent = 19;
optional CallLinkUpdate callLinkUpdate = 20;
optional CallLogEvent callLogEvent = 21;
optional DeleteForMe deleteForMe = 22;
optional DeviceNameChange deviceNameChange = 23;
optional AttachmentBackfillRequest attachmentBackfillRequest = 24;
optional AttachmentBackfillResponse attachmentBackfillResponse = 25;
}
message AttachmentPointer {
@ -935,7 +801,6 @@ message ContactDetails {
optional string number = 1;
optional string aci = 9;
optional bytes aciBinary = 13; // 16-byte UUID
optional string name = 2;
optional Avatar avatar = 3;
reserved /* color */ 4;
@ -946,7 +811,7 @@ message ContactDetails {
optional uint32 expireTimerVersion = 12;
optional uint32 inboxPosition = 10;
reserved /* archived */ 11;
// NEXT ID: 14
// NEXT ID: 13
}
message PaymentAddress {
@ -993,7 +858,6 @@ message BodyRange {
oneof associatedValue {
string mentionAci = 3;
Style style = 4;
bytes mentionAciBinary = 5; // 16-byte UUID
}
}
@ -1001,7 +865,6 @@ message AddressableMessage {
oneof author {
string authorServiceId = 1;
string authorE164 = 2;
bytes authorServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
optional uint64 sentTimestamp = 3;
}
@ -1011,6 +874,5 @@ message ConversationIdentifier {
string threadServiceId = 1;
bytes threadGroupId = 2;
string threadE164 = 3;
bytes threadServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
}

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.1
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: StickerResources.proto
package signalpb

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.1
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: StorageService.proto
package signalpb
@ -1086,9 +1086,7 @@ type ContactRecord struct {
PniSignatureVerified bool `protobuf:"varint,21,opt,name=pniSignatureVerified,proto3" json:"pniSignatureVerified,omitempty"`
Nickname *ContactRecord_Name `protobuf:"bytes,22,opt,name=nickname,proto3" json:"nickname,omitempty"`
Note string `protobuf:"bytes,23,opt,name=note,proto3" json:"note,omitempty"`
AvatarColor *AvatarColor `protobuf:"varint,24,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
AciBinary []byte `protobuf:"bytes,25,opt,name=aciBinary,proto3" json:"aciBinary,omitempty"` // 16-byte UUID
PniBinary []byte `protobuf:"bytes,26,opt,name=pniBinary,proto3" json:"pniBinary,omitempty"` // 16-byte UUID
AvatarColor *AvatarColor `protobuf:"varint,24,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"` // Next ID: 25
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1291,20 +1289,6 @@ func (x *ContactRecord) GetAvatarColor() AvatarColor {
return AvatarColor_A100
}
func (x *ContactRecord) GetAciBinary() []byte {
if x != nil {
return x.AciBinary
}
return nil
}
func (x *ContactRecord) GetPniBinary() []byte {
if x != nil {
return x.PniBinary
}
return nil
}
type GroupV1Record struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@ -1401,7 +1385,6 @@ type GroupV2Record struct {
HideStory bool `protobuf:"varint,8,opt,name=hideStory,proto3" json:"hideStory,omitempty"`
StorySendMode GroupV2Record_StorySendMode `protobuf:"varint,10,opt,name=storySendMode,proto3,enum=signalservice.GroupV2Record_StorySendMode" json:"storySendMode,omitempty"`
AvatarColor *AvatarColor `protobuf:"varint,11,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
VerifiedNameHash []byte `protobuf:"bytes,12,opt,name=verifiedNameHash,proto3" json:"verifiedNameHash,omitempty"` // SHA-256 of UTF-8 encoded decrypted group title that was last verified
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1506,13 +1489,6 @@ func (x *GroupV2Record) GetAvatarColor() AvatarColor {
return AvatarColor_A100
}
func (x *GroupV2Record) GetVerifiedNameHash() []byte {
if x != nil {
return x.VerifiedNameHash
}
return nil
}
type Payments struct {
state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
@ -1604,9 +1580,6 @@ type AccountRecord struct {
AvatarColor *AvatarColor `protobuf:"varint,42,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
BackupTierHistory *AccountRecord_BackupTierHistory `protobuf:"bytes,43,opt,name=backupTierHistory,proto3" json:"backupTierHistory,omitempty"`
NotificationProfileManualOverride *AccountRecord_NotificationProfileManualOverride `protobuf:"bytes,44,opt,name=notificationProfileManualOverride,proto3" json:"notificationProfileManualOverride,omitempty"`
NotificationProfileSyncDisabled bool `protobuf:"varint,45,opt,name=notificationProfileSyncDisabled,proto3" json:"notificationProfileSyncDisabled,omitempty"`
AutomaticKeyVerificationDisabled bool `protobuf:"varint,46,opt,name=automaticKeyVerificationDisabled,proto3" json:"automaticKeyVerificationDisabled,omitempty"`
HasSeenAdminDeleteEducationDialog bool `protobuf:"varint,47,opt,name=hasSeenAdminDeleteEducationDialog,proto3" json:"hasSeenAdminDeleteEducationDialog,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1900,38 +1873,16 @@ func (x *AccountRecord) GetNotificationProfileManualOverride() *AccountRecord_No
return nil
}
func (x *AccountRecord) GetNotificationProfileSyncDisabled() bool {
if x != nil {
return x.NotificationProfileSyncDisabled
}
return false
}
func (x *AccountRecord) GetAutomaticKeyVerificationDisabled() bool {
if x != nil {
return x.AutomaticKeyVerificationDisabled
}
return false
}
func (x *AccountRecord) GetHasSeenAdminDeleteEducationDialog() bool {
if x != nil {
return x.HasSeenAdminDeleteEducationDialog
}
return false
}
type StoryDistributionListRecord struct {
state protoimpl.MessageState `protogen:"open.v1"`
Identifier []byte `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
RecipientServiceIds []string `protobuf:"bytes,3,rep,name=recipientServiceIds,proto3" json:"recipientServiceIds,omitempty"`
DeletedAtTimestamp uint64 `protobuf:"varint,4,opt,name=deletedAtTimestamp,proto3" json:"deletedAtTimestamp,omitempty"`
AllowsReplies bool `protobuf:"varint,5,opt,name=allowsReplies,proto3" json:"allowsReplies,omitempty"`
IsBlockList bool `protobuf:"varint,6,opt,name=isBlockList,proto3" json:"isBlockList,omitempty"`
RecipientServiceIdsBinary [][]byte `protobuf:"bytes,7,rep,name=recipientServiceIdsBinary,proto3" json:"recipientServiceIdsBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Identifier []byte `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
RecipientServiceIds []string `protobuf:"bytes,3,rep,name=recipientServiceIds,proto3" json:"recipientServiceIds,omitempty"`
DeletedAtTimestamp uint64 `protobuf:"varint,4,opt,name=deletedAtTimestamp,proto3" json:"deletedAtTimestamp,omitempty"`
AllowsReplies bool `protobuf:"varint,5,opt,name=allowsReplies,proto3" json:"allowsReplies,omitempty"`
IsBlockList bool `protobuf:"varint,6,opt,name=isBlockList,proto3" json:"isBlockList,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StoryDistributionListRecord) Reset() {
@ -2006,18 +1957,12 @@ func (x *StoryDistributionListRecord) GetIsBlockList() bool {
return false
}
func (x *StoryDistributionListRecord) GetRecipientServiceIdsBinary() [][]byte {
if x != nil {
return x.RecipientServiceIdsBinary
}
return nil
}
type CallLinkRecord struct {
state protoimpl.MessageState `protogen:"open.v1"`
RootKey []byte `protobuf:"bytes,1,opt,name=rootKey,proto3" json:"rootKey,omitempty"`
AdminPasskey []byte `protobuf:"bytes,2,opt,name=adminPasskey,proto3" json:"adminPasskey,omitempty"`
DeletedAtTimestampMs uint64 `protobuf:"varint,3,opt,name=deletedAtTimestampMs,proto3" json:"deletedAtTimestampMs,omitempty"`
Epoch []byte `protobuf:"bytes,4,opt,name=epoch,proto3,oneof" json:"epoch,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -2073,6 +2018,13 @@ func (x *CallLinkRecord) GetDeletedAtTimestampMs() uint64 {
return 0
}
func (x *CallLinkRecord) GetEpoch() []byte {
if x != nil {
return x.Epoch
}
return nil
}
type Recipient struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Identifier:
@ -2931,12 +2883,11 @@ func (*AccountRecord_NotificationProfileManualOverride_Enabled) isAccountRecord_
}
type AccountRecord_PinnedConversation_Contact struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
ServiceIdBinary []byte `protobuf:"bytes,3,opt,name=serviceIdBinary,proto3" json:"serviceIdBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AccountRecord_PinnedConversation_Contact) Reset() {
@ -2983,13 +2934,6 @@ func (x *AccountRecord_PinnedConversation_Contact) GetE164() string {
return ""
}
func (x *AccountRecord_PinnedConversation_Contact) GetServiceIdBinary() []byte {
if x != nil {
return x.ServiceIdBinary
}
return nil
}
type AccountRecord_NotificationProfileManualOverride_ManuallyEnabled struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@ -3044,12 +2988,11 @@ func (x *AccountRecord_NotificationProfileManualOverride_ManuallyEnabled) GetEnd
}
type Recipient_Contact struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
ServiceIdBinary []byte `protobuf:"bytes,3,opt,name=serviceIdBinary,proto3" json:"serviceIdBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Recipient_Contact) Reset() {
@ -3096,13 +3039,6 @@ func (x *Recipient_Contact) GetE164() string {
return ""
}
func (x *Recipient_Contact) GetServiceIdBinary() []byte {
if x != nil {
return x.ServiceIdBinary
}
return nil
}
var File_StorageService_proto protoreflect.FileDescriptor
const file_StorageService_proto_rawDesc = "" +
@ -3155,7 +3091,7 @@ const file_StorageService_proto_rawDesc = "" +
"chatFolder\x18\b \x01(\v2\x1f.signalservice.ChatFolderRecordH\x00R\n" +
"chatFolder\x12V\n" +
"\x13notificationProfile\x18\t \x01(\v2\".signalservice.NotificationProfileH\x00R\x13notificationProfileB\b\n" +
"\x06record\"\xd9\b\n" +
"\x06record\"\x9d\b\n" +
"\rContactRecord\x12\x10\n" +
"\x03aci\x18\x01 \x01(\tR\x03aci\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12\x10\n" +
@ -3185,9 +3121,7 @@ const file_StorageService_proto_rawDesc = "" +
"\x14pniSignatureVerified\x18\x15 \x01(\bR\x14pniSignatureVerified\x12=\n" +
"\bnickname\x18\x16 \x01(\v2!.signalservice.ContactRecord.NameR\bnickname\x12\x12\n" +
"\x04note\x18\x17 \x01(\tR\x04note\x12A\n" +
"\vavatarColor\x18\x18 \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x12\x1c\n" +
"\taciBinary\x18\x19 \x01(\fR\taciBinary\x12\x1c\n" +
"\tpniBinary\x18\x1a \x01(\fR\tpniBinary\x1a4\n" +
"\vavatarColor\x18\x18 \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x1a4\n" +
"\x04Name\x12\x14\n" +
"\x05given\x18\x01 \x01(\tR\x05given\x12\x16\n" +
"\x06family\x18\x02 \x01(\tR\x06family\":\n" +
@ -3203,7 +3137,7 @@ const file_StorageService_proto_rawDesc = "" +
"\vwhitelisted\x18\x03 \x01(\bR\vwhitelisted\x12\x1a\n" +
"\barchived\x18\x04 \x01(\bR\barchived\x12\"\n" +
"\fmarkedUnread\x18\x05 \x01(\bR\fmarkedUnread\x120\n" +
"\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xcd\x04\n" +
"\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xa1\x04\n" +
"\rGroupV2Record\x12\x1c\n" +
"\tmasterKey\x18\x01 \x01(\fR\tmasterKey\x12\x18\n" +
"\ablocked\x18\x02 \x01(\bR\ablocked\x12 \n" +
@ -3215,8 +3149,7 @@ const file_StorageService_proto_rawDesc = "" +
"\thideStory\x18\b \x01(\bR\thideStory\x12P\n" +
"\rstorySendMode\x18\n" +
" \x01(\x0e2*.signalservice.GroupV2Record.StorySendModeR\rstorySendMode\x12A\n" +
"\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x12*\n" +
"\x10verifiedNameHash\x18\f \x01(\fR\x10verifiedNameHash\"7\n" +
"\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\"7\n" +
"\rStorySendMode\x12\v\n" +
"\aDEFAULT\x10\x00\x12\f\n" +
"\bDISABLED\x10\x01\x12\v\n" +
@ -3225,7 +3158,7 @@ const file_StorageService_proto_rawDesc = "" +
"\">\n" +
"\bPayments\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" +
"\aentropy\x18\x02 \x01(\fR\aentropy\"\x99\x1d\n" +
"\aentropy\x18\x02 \x01(\fR\aentropy\"\x8b\x1b\n" +
"\rAccountRecord\x12\x1e\n" +
"\n" +
"profileKey\x18\x01 \x01(\fR\n" +
@ -3270,18 +3203,14 @@ const file_StorageService_proto_rawDesc = "" +
"\x14backupSubscriberData\x18) \x01(\v2..signalservice.AccountRecord.IAPSubscriberDataR\x14backupSubscriberData\x12A\n" +
"\vavatarColor\x18* \x01(\x0e2\x1a.signalservice.AvatarColorH\x02R\vavatarColor\x88\x01\x01\x12\\\n" +
"\x11backupTierHistory\x18+ \x01(\v2..signalservice.AccountRecord.BackupTierHistoryR\x11backupTierHistory\x12\x8c\x01\n" +
"!notificationProfileManualOverride\x18, \x01(\v2>.signalservice.AccountRecord.NotificationProfileManualOverrideR!notificationProfileManualOverride\x12H\n" +
"\x1fnotificationProfileSyncDisabled\x18- \x01(\bR\x1fnotificationProfileSyncDisabled\x12J\n" +
" automaticKeyVerificationDisabled\x18. \x01(\bR automaticKeyVerificationDisabled\x12L\n" +
"!hasSeenAdminDeleteEducationDialog\x18/ \x01(\bR!hasSeenAdminDeleteEducationDialog\x1a\xb0\x02\n" +
"!notificationProfileManualOverride\x18, \x01(\v2>.signalservice.AccountRecord.NotificationProfileManualOverrideR!notificationProfileManualOverride\x1a\x86\x02\n" +
"\x12PinnedConversation\x12S\n" +
"\acontact\x18\x01 \x01(\v27.signalservice.AccountRecord.PinnedConversation.ContactH\x00R\acontact\x12&\n" +
"\rlegacyGroupId\x18\x03 \x01(\fH\x00R\rlegacyGroupId\x12(\n" +
"\x0egroupMasterKey\x18\x04 \x01(\fH\x00R\x0egroupMasterKey\x1ae\n" +
"\x0egroupMasterKey\x18\x04 \x01(\fH\x00R\x0egroupMasterKey\x1a;\n" +
"\aContact\x12\x1c\n" +
"\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12(\n" +
"\x0fserviceIdBinary\x18\x03 \x01(\fR\x0fserviceIdBinaryB\f\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164B\f\n" +
"\n" +
"identifier\x1a\xf8\x01\n" +
"\fUsernameLink\x12\x18\n" +
@ -3329,7 +3258,7 @@ const file_StorageService_proto_rawDesc = "" +
"_hasBackupB\r\n" +
"\v_backupTierB\x0e\n" +
"\f_avatarColorJ\x04\b\t\x10\n" +
"J\x04\b\x13\x10\x14J\x04\b\x1c\x10\x1dJ\x04\b\x1f\x10 J\x04\b$\x10%J\x04\b%\x10&J\x04\b&\x10'\"\xb9\x02\n" +
"J\x04\b\x13\x10\x14J\x04\b\x1c\x10\x1dJ\x04\b\x1f\x10 J\x04\b$\x10%J\x04\b%\x10&J\x04\b&\x10'\"\xfb\x01\n" +
"\x1bStoryDistributionListRecord\x12\x1e\n" +
"\n" +
"identifier\x18\x01 \x01(\fR\n" +
@ -3338,20 +3267,20 @@ const file_StorageService_proto_rawDesc = "" +
"\x13recipientServiceIds\x18\x03 \x03(\tR\x13recipientServiceIds\x12.\n" +
"\x12deletedAtTimestamp\x18\x04 \x01(\x04R\x12deletedAtTimestamp\x12$\n" +
"\rallowsReplies\x18\x05 \x01(\bR\rallowsReplies\x12 \n" +
"\visBlockList\x18\x06 \x01(\bR\visBlockList\x12<\n" +
"\x19recipientServiceIdsBinary\x18\a \x03(\fR\x19recipientServiceIdsBinary\"\x88\x01\n" +
"\visBlockList\x18\x06 \x01(\bR\visBlockList\"\xa7\x01\n" +
"\x0eCallLinkRecord\x12\x18\n" +
"\arootKey\x18\x01 \x01(\fR\arootKey\x12\"\n" +
"\fadminPasskey\x18\x02 \x01(\fR\fadminPasskey\x122\n" +
"\x14deletedAtTimestampMs\x18\x03 \x01(\x04R\x14deletedAtTimestampMsJ\x04\b\x04\x10\x05\"\x90\x02\n" +
"\x14deletedAtTimestampMs\x18\x03 \x01(\x04R\x14deletedAtTimestampMs\x12\x19\n" +
"\x05epoch\x18\x04 \x01(\fH\x00R\x05epoch\x88\x01\x01B\b\n" +
"\x06_epoch\"\xe6\x01\n" +
"\tRecipient\x12<\n" +
"\acontact\x18\x01 \x01(\v2 .signalservice.Recipient.ContactH\x00R\acontact\x12&\n" +
"\rlegacyGroupId\x18\x02 \x01(\fH\x00R\rlegacyGroupId\x12(\n" +
"\x0egroupMasterKey\x18\x03 \x01(\fH\x00R\x0egroupMasterKey\x1ae\n" +
"\x0egroupMasterKey\x18\x03 \x01(\fH\x00R\x0egroupMasterKey\x1a;\n" +
"\aContact\x12\x1c\n" +
"\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12(\n" +
"\x0fserviceIdBinary\x18\x03 \x01(\fR\x0fserviceIdBinaryB\f\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164B\f\n" +
"\n" +
"identifier\"\xe8\x04\n" +
"\x10ChatFolderRecord\x12\x1e\n" +
@ -3539,6 +3468,7 @@ func file_StorageService_proto_init() {
file_StorageService_proto_msgTypes[7].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[9].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[11].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[13].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[14].OneofWrappers = []any{
(*Recipient_Contact_)(nil),
(*Recipient_LegacyGroupId)(nil),

View file

@ -140,9 +140,7 @@ message ContactRecord {
Name nickname = 22;
string note = 23;
optional AvatarColor avatarColor = 24;
bytes aciBinary = 25; // 16-byte UUID
bytes pniBinary = 26; // 16-byte UUID
// Next ID: 27
// Next ID: 25
}
message GroupV1Record {
@ -172,7 +170,6 @@ message GroupV2Record {
reserved /* storySendEnabled */ 9;
StorySendMode storySendMode = 10;
optional AvatarColor avatarColor = 11;
bytes verifiedNameHash = 12; // SHA-256 of UTF-8 encoded decrypted group title that was last verified
}
message Payments {
@ -190,9 +187,8 @@ message AccountRecord {
message PinnedConversation {
message Contact {
string serviceId = 1;
string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
string serviceId = 1;
string e164 = 2;
}
oneof identifier {
@ -295,33 +291,28 @@ message AccountRecord {
optional AvatarColor avatarColor = 42;
BackupTierHistory backupTierHistory = 43;
NotificationProfileManualOverride notificationProfileManualOverride = 44;
bool notificationProfileSyncDisabled = 45;
bool automaticKeyVerificationDisabled = 46;
bool hasSeenAdminDeleteEducationDialog = 47;
}
message StoryDistributionListRecord {
bytes identifier = 1;
string name = 2;
repeated string recipientServiceIds = 3;
uint64 deletedAtTimestamp = 4;
bool allowsReplies = 5;
bool isBlockList = 6;
repeated bytes recipientServiceIdsBinary = 7; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
bytes identifier = 1;
string name = 2;
repeated string recipientServiceIds = 3;
uint64 deletedAtTimestamp = 4;
bool allowsReplies = 5;
bool isBlockList = 6;
}
message CallLinkRecord {
bytes rootKey = 1;
bytes adminPasskey = 2;
uint64 deletedAtTimestampMs = 3;
reserved 4; // was epoch field, never used
optional bytes epoch = 4;
}
message Recipient {
message Contact {
string serviceId = 1;
string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
string serviceId = 1;
string e164 = 2;
}
oneof identifier {

View file

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

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.34.1
// protoc-gen-go v1.36.6
// protoc v3.21.12
// source: WebSocketResources.proto
package signalpb

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ syntax = "proto3";
package signal.backup;
option java_package = "org.signal.archive.proto";
option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
option swift_prefix = "BackupProto_";
message BackupInfo {
@ -68,40 +68,6 @@ message AccountData {
Color color = 3;
}
enum SentMediaQuality {
UNKNOWN_QUALITY = 0; // Interpret as "Standard"
STANDARD = 1;
HIGH = 2;
}
message AutoDownloadSettings {
enum AutoDownloadOption {
UNKNOWN = 0; // Interpret as "Never"
NEVER = 1;
WIFI = 2;
WIFI_AND_CELLULAR = 3;
}
AutoDownloadOption images = 1;
AutoDownloadOption audio = 2;
AutoDownloadOption video = 3;
AutoDownloadOption documents = 4;
}
enum AppTheme {
UNKNOWN_APP_THEME = 0; // Interpret as "System"
SYSTEM = 1;
LIGHT = 2;
DARK = 3;
}
enum CallsUseLessDataSetting {
UNKNOWN_CALL_DATA_SETTING = 0; // Interpret as "Never"
NEVER = 1;
MOBILE_DATA_ONLY = 2;
WIFI_AND_MOBILE_DATA = 3;
}
message AccountSettings {
bool readReceipts = 1;
bool sealedSenderIndicators = 2;
@ -125,17 +91,6 @@ message AccountData {
bool optimizeOnDeviceStorage = 20;
// See zkgroup for integer particular values. Unset if backups are not enabled.
optional uint64 backupTier = 21;
reserved /* showSealedSenderIndicators */ 22;
SentMediaQuality defaultSentMediaQuality = 23;
AutoDownloadSettings autoDownloadSettings = 24;
reserved /* wifiAutoDownloadSettings */ 25;
optional uint32 screenLockTimeoutMinutes = 26; // If unset, consider screen lock to be disabled.
optional bool pinReminders = 27; // If unset, consider pin reminders to be enabled.
AppTheme appTheme = 28; // If unset, treat the same as "Unknown" case
CallsUseLessDataSetting callsUseLessDataSetting = 29; // If unset, treat the same as "Unknown" case
bool allowSealedSenderFromAnyone = 30;
bool allowAutomaticKeyVerification = 31;
bool hasSeenAdminDeleteEducationDialog = 32;
}
message SubscriberData {
@ -156,18 +111,6 @@ message AccountData {
}
}
message AndroidSpecificSettings {
enum NavigationBarSize {
UNKNOWN_BAR_SIZE = 0; // Intepret as "Normal"
NORMAL = 1;
COMPACT = 2;
}
bool useSystemEmoji = 1;
bool screenshotSecurity = 2;
NavigationBarSize navigationBarSize = 3; // If unset, treat the same as "Unknown" case
}
bytes profileKey = 1;
optional string username = 2;
UsernameLink usernameLink = 3;
@ -179,10 +122,6 @@ message AccountData {
AccountSettings accountSettings = 9;
IAPSubscriberData backupsSubscriberData = 10;
string svrPin = 11;
AndroidSpecificSettings androidSpecificSettings = 12;
string bioText = 13;
string bioEmoji = 14;
optional bytes keyTransparencyData = 15;
}
message Recipient {
@ -271,7 +210,6 @@ message Contact {
string systemFamilyName = 19;
string systemNickname = 20;
optional AvatarColor avatarColor = 21;
optional bytes keyTransparencyData = 22;
}
message Group {
@ -308,7 +246,6 @@ message Group {
bytes inviteLinkPassword = 10;
bool announcements_only = 12;
repeated MemberBanned members_banned = 13;
bool terminated = 14;
}
message GroupAttributeBlob {
@ -333,8 +270,6 @@ message Group {
reserved /*profileKey*/ 3; // This field is ignored in Backups, in favor of Contact frames for members
reserved /*presentation*/ 4; // This field is deprecated in the context of static group state
uint32 joinedAtVersion = 5;
string labelEmoji = 6;
string labelString = 7;
}
message MemberPendingProfileKey {
@ -367,7 +302,6 @@ message Group {
AccessRequired attributes = 1;
AccessRequired members = 2;
AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
}
}
@ -410,7 +344,7 @@ message CallLink {
string name = 3;
Restrictions restrictions = 4;
uint64 expirationMs = 5;
reserved /*epoch*/ 6;
optional bytes epoch = 6; // May be absent/empty for older links
}
message AdHocCall {
@ -468,14 +402,6 @@ message ChatItem {
message DirectionlessMessageDetails {
}
message PinDetails {
uint64 pinnedAtTimestamp = 1;
oneof pinExpiry {
uint64 pinExpiresAtTimestamp = 2; // timestamp when the pin should expire
bool pinNeverExpires = 3;
}
}
uint64 chatId = 1; // conversation id
uint64 authorId = 2; // recipient id
uint64 dateSent = 3;
@ -502,11 +428,7 @@ message ChatItem {
GiftBadge giftBadge = 17;
ViewOnceMessage viewOnceMessage = 18;
DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up
Poll poll = 20;
AdminDeletedMessage adminDeletedMessage = 22;
}
PinDetails pinDetails = 21; // only set if message is pinned
}
message SendStatus {
@ -837,7 +759,6 @@ message Quote {
NORMAL = 1;
GIFT_BADGE = 2;
VIEW_ONCE = 3;
POLL = 4;
}
message QuotedAttachment {
@ -884,30 +805,6 @@ message Reaction {
uint64 sortOrder = 4;
}
message Poll {
message PollOption {
message PollVote {
uint64 voterId = 1; // A direct reference to Recipient proto id. Must be self or contact.
uint32 voteCount = 2; // Tracks how many times you voted.
}
string option = 1; // Between 1-100 characters
repeated PollVote votes = 2;
}
string question = 1; // Between 1-100 characters
bool allowMultiple = 2;
repeated PollOption options = 3; // At least two
bool hasEnded = 4;
repeated Reaction reactions = 5;
}
message AdminDeletedMessage {
uint64 adminId = 1; // id of the admin that deleted the message
}
message ChatUpdateMessage {
// If unset, importers should ignore the update message without throwing an error.
oneof update {
@ -920,8 +817,6 @@ message ChatUpdateMessage {
IndividualCall individualCall = 7;
GroupCall groupCall = 8;
LearnedProfileChatUpdate learnedProfileChange = 9;
PollTerminateUpdate pollTerminate = 10;
PinMessageUpdate pinMessage = 11;
}
}
@ -1078,8 +973,6 @@ message GroupChangeChatUpdate {
GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32;
GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33;
GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35;
GroupTerminateChangeUpdate groupTerminateChangeUpdate = 36;
}
}
@ -1131,15 +1024,6 @@ message GroupAttributesAccessLevelChangeUpdate {
GroupV2AccessLevel accessLevel = 2;
}
message GroupMemberLabelAccessLevelChangeUpdate {
optional bytes updaterAci = 1;
GroupV2AccessLevel accessLevel = 2;
}
message GroupTerminateChangeUpdate {
optional bytes updaterAci = 1;
}
message GroupAnnouncementOnlyChangeUpdate {
optional bytes updaterAci = 1;
bool isAnnouncementOnly = 2;
@ -1298,16 +1182,6 @@ message GroupExpirationTimerUpdate {
optional bytes updaterAci = 2;
}
message PollTerminateUpdate {
uint64 targetSentTimestamp = 1;
string question = 2; // Between 1-100 characters
}
message PinMessageUpdate {
uint64 targetSentTimestamp = 1;
uint64 authorId = 2; // recipient id
}
message StickerPack {
bytes packId = 1;
bytes packKey = 2;

View file

@ -1,19 +1,19 @@
#!/bin/bash
set -euo pipefail
ANDROID_GIT_REVISION=${1:-439760e7732585bfd078d92d93732c04cc31e29e}
DESKTOP_GIT_REVISION=${1:-1b2a3e7b283c32c5654a39da12fc04139fd26dbd}
ANDROID_GIT_REVISION=${1:-62fdf3d1aa9f637729ae67b55aadcc24f38f0117}
DESKTOP_GIT_REVISION=${1:-203a1cc5e3f9c1533a58caff72e13aa6eaeeddc7}
update_proto() {
case "$1" in
Signal-Android)
REPO="Signal-Android"
prefix="lib/libsignal-service/src/main/protowire/"
prefix="libsignal-service/src/main/protowire/"
GIT_REVISION=$ANDROID_GIT_REVISION
;;
Signal-Android-Archive)
Signal-Android-App)
REPO="Signal-Android"
prefix="lib/archive/src/main/protowire/"
prefix="app/src/main/protowire/"
GIT_REVISION=$ANDROID_GIT_REVISION
;;
Signal-Desktop)
@ -34,10 +34,10 @@ update_proto Signal-Android StickerResources.proto
update_proto Signal-Android WebSocketResources.proto
update_proto Signal-Android StorageService.proto
update_proto Signal-Android-Archive Backup.proto
update_proto Signal-Android-App Backup.proto
mv Backup.proto backuppb/Backup.proto
update_proto Signal-Desktop DeviceName.proto
# TODO these were moved to libsignal only
#update_proto Signal-Desktop UnidentifiedDelivery.proto
#update_proto Signal-Desktop ContactDiscovery.proto
update_proto Signal-Desktop UnidentifiedDelivery.proto
# Android has CDSI.proto too, but the types have more generic names (since android uses a different package name)
update_proto Signal-Desktop ContactDiscovery.proto

View file

@ -18,10 +18,11 @@ package signalmeow
import (
"context"
"crypto/hmac"
"encoding/base64"
"encoding/json"
"fmt"
mrand "math/rand/v2"
mrand "math/rand"
"net/http"
"net/url"
"time"
@ -53,6 +54,7 @@ const (
StateProvisioningError ProvisioningState = iota
StateProvisioningURLReceived
StateProvisioningDataReceived
StateProvisioningPreKeysRegistered
)
func (s ProvisioningState) String() string {
@ -63,6 +65,8 @@ func (s ProvisioningState) String() string {
return "StateProvisioningURLReceived"
case StateProvisioningDataReceived:
return "StateProvisioningDataReceived"
case StateProvisioningPreKeysRegistered:
return "StateProvisioningPreKeysRegistered"
default:
return fmt.Sprintf("ProvisioningState(%d)", s)
}
@ -124,8 +128,8 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
username := *provisioningMessage.Number
password := random.String(22)
code := provisioningMessage.ProvisioningCode
aciRegistrationID := mrand.IntN(16383) + 1
pniRegistrationID := mrand.IntN(16383) + 1
aciRegistrationID := mrand.Intn(16383) + 1
pniRegistrationID := mrand.Intn(16383) + 1
aciSignedPreKey := GenerateSignedPreKey(1, aciIdentityKeyPair)
pniSignedPreKey := GenerateSignedPreKey(1, pniIdentityKeyPair)
aciPQLastResortPreKey := GenerateKyberPreKeys(1, 1, aciIdentityKeyPair)[0]
@ -165,19 +169,24 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
DeviceID: deviceId,
Number: *provisioningMessage.Number,
Password: password,
MasterKey: provisioningMessage.GetMasterKey(),
AccountEntropyPool: libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()),
EphemeralBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetEphemeralBackupKey()),
MediaRootBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetMediaRootBackupKey()),
}
if provisioningMessage.GetAccountEntropyPool() != "" {
data.MasterKey, err = libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()).DeriveSVRKey()
var masterKey []byte
masterKey, err = libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()).DeriveSVRKey()
if err != nil {
log.Err(err).Msg("Failed to derive master key from account entropy pool")
} else {
log.Debug().Msg("Derived master key from account entropy pool")
}
} else {
log.Warn().Msg("No account entropy pool in provisioning message")
if data.MasterKey == nil {
data.MasterKey = masterKey
} else if !hmac.Equal(data.MasterKey, masterKey) {
log.Warn().Msg("Master key mismatch")
}
}
// Store the provisioning data
@ -241,6 +250,28 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
// Return the provisioning data
c <- ProvisioningResponse{State: StateProvisioningDataReceived, ProvisioningData: data}
// Generate, store, and register prekeys
// TODO hacky client construction
cli := &Client{Store: device}
err = cli.GenerateAndRegisterPreKeys(ctx, device.ACIPreKeyStore)
if err != nil {
c <- ProvisioningResponse{
State: StateProvisioningError,
Err: fmt.Errorf("error generating and registering ACI prekeys: %w", err),
}
return
}
err = cli.GenerateAndRegisterPreKeys(ctx, device.PNIPreKeyStore)
if err != nil {
c <- ProvisioningResponse{
State: StateProvisioningError,
Err: fmt.Errorf("error generating and registering PNI prekeys: %w", err),
}
return
}
c <- ProvisioningResponse{State: StateProvisioningPreKeysRegistered}
}()
return c
}
@ -268,7 +299,7 @@ func startProvisioning(ctx context.Context, ws *websocket.Conn, provisioningCiph
return "", fmt.Errorf("failed to unmarshal provisioning UUID: %w", err)
}
linkCapabilities := []string{"backup4,backup5"}
linkCapabilities := []string{"backup4", "backup5"}
if !allowBackup {
linkCapabilities = []string{}
}
@ -335,19 +366,36 @@ var signalCapabilities = map[string]any{
var signalCapabilitiesBody = exerrors.Must(json.Marshal(signalCapabilities))
func (cli *Client) RegisterCapabilities(ctx context.Context) error {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/devices/capabilities", signalCapabilitiesBody, nil)
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, "/v1/devices/capabilities", &web.HTTPReqOpt{
Body: signalCapabilitiesBody,
Username: &username,
Password: &password,
ContentType: web.ContentTypeJSON,
})
if resp != nil {
_ = resp.Body.Close()
}
if err != nil {
return err
} else if resp.StatusCode >= 400 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
return web.DecodeWSResponseBody(ctx, nil, resp)
return nil
}
func (cli *Client) Unlink(ctx context.Context) error {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodDelete, fmt.Sprintf("/v1/devices/%d", cli.Store.DeviceID), nil, nil)
username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/v1/devices/%d", cli.Store.DeviceID), &web.HTTPReqOpt{
Username: &username,
Password: &password,
})
if err != nil {
return err
} else if resp.StatusCode >= 400 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
return web.DecodeWSResponseBody(ctx, nil, resp)
return nil
}
func confirmDevice(

View file

@ -19,9 +19,9 @@ package signalmeow
import (
"context"
"encoding/json"
"fmt"
"net/http"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
@ -36,21 +36,30 @@ type ReqRegisterAPNs struct {
}
func (cli *Client) registerPush(ctx context.Context, pushType string, data any) error {
var resp *signalpb.WebSocketResponseMessage
var err error
username, password := cli.Store.BasicAuthCreds()
req := &web.HTTPReqOpt{
Username: &username,
Password: &password,
}
var method string
if data != nil {
body, err := json.Marshal(data)
method = http.MethodPut
req.ContentType = web.ContentTypeJSON
var err error
req.Body, err = json.Marshal(data)
if err != nil {
return err
}
resp, err = cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/accounts/"+pushType, body, nil)
} else {
resp, err = cli.AuthedWS.SendRequest(ctx, http.MethodDelete, "/v1/accounts/"+pushType, nil, nil)
method = http.MethodDelete
}
resp, err := web.SendHTTPRequest(ctx, method, "/v1/accounts/"+pushType, req)
if err != nil {
return err
} else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
return web.DecodeWSResponseBody(ctx, nil, resp)
return nil
}
func (cli *Client) RegisterFCM(ctx context.Context, token string) error {

View file

@ -29,7 +29,6 @@ import (
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
@ -250,39 +249,32 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
cli.loopWg.Add(1)
go func() {
defer cli.loopWg.Done()
select {
case <-loopCtx.Done():
return
case <-initialConnectChan:
log.Info().Msg("Both websockets connected, sending contacts sync request")
err = cli.RegisterCapabilities(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
} else {
zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
}
// Start loop to check for and upload more prekeys
cli.loopWg.Add(1)
go func() {
defer cli.loopWg.Done()
cli.keyCheckLoop(loopCtx)
}()
// TODO hacky
if cli.SyncContactsOnConnect {
cli.SendContactSyncRequest(loopCtx)
}
if cli.Store.MasterKey == nil {
cli.SendStorageMasterKeyRequest(loopCtx)
for {
select {
case <-loopCtx.Done():
return
case <-initialConnectChan:
log.Info().Msg("Both websockets connected, sending contacts sync request")
// TODO hacky
if cli.SyncContactsOnConnect {
cli.SendContactSyncRequest(loopCtx)
}
if cli.Store.MasterKey == nil {
cli.SendStorageMasterKeyRequest(loopCtx)
}
return
}
}
}()
return statusChan, nil
}
// Start loop to check for and upload more prekeys
cli.loopWg.Add(1)
go func() {
defer cli.loopWg.Done()
cli.keyCheckLoop(loopCtx)
}()
func (cli *Client) ForceReconnect() {
cli.AuthedWS.ForceReconnect()
cli.UnauthedWS.ForceReconnect()
return statusChan, nil
}
func (cli *Client) StopReceiveLoops() error {
@ -357,25 +349,21 @@ func (cli *Client) incomingAPIMessageHandler(ctx context.Context, req *signalpb.
return nil, err
}
log = log.With().
Uint64("envelope_timestamp", envelope.GetClientTimestamp()).
Uint64("envelope_timestamp", envelope.GetTimestamp()).
Uint64("server_timestamp", envelope.GetServerTimestamp()).
Logger()
ctx = log.WithContext(ctx)
destinationServiceID, _ := ParseStringOrBinaryServiceID(envelope.GetDestinationServiceId(), envelope.GetDestinationServiceIdBinary())
sourceServiceID, _ := ParseStringOrBinaryServiceID(envelope.GetSourceServiceId(), envelope.GetSourceServiceIdBinary())
destinationServiceID, err := libsignalgo.ServiceIDFromString(envelope.GetDestinationServiceId())
log.Debug().
Str("destination_service_id", envelope.GetDestinationServiceId()).
Str("source_service_id", envelope.GetSourceServiceId()).
Hex("destination_service_id_bytes", envelope.GetDestinationServiceIdBinary()).
Hex("source_service_id_bytes", envelope.GetSourceServiceIdBinary()).
Uint32("source_device_id", envelope.GetSourceDeviceId()).
Uint32("source_device_id", envelope.GetSourceDevice()).
Object("parsed_destination_service_id", destinationServiceID).
Object("parsed_source_service_id", sourceServiceID).
Int32("envelope_type_id", int32(envelope.GetType())).
Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]).
Msg("Received envelope")
result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID)
result := cli.decryptEnvelope(ctx, envelope)
err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID)
if err != nil {
@ -407,7 +395,7 @@ func (cli *Client) handleDecryptedResult(
result DecryptionResult,
envelope *signalpb.Envelope,
destinationServiceID libsignalgo.ServiceID,
) (retErr error) {
) error {
if errors.Is(result.Err, context.Canceled) {
return result.Err
} else if ctx.Err() != nil {
@ -429,58 +417,37 @@ func (cli *Client) handleDecryptedResult(
}()
}
var theirServiceID libsignalgo.ServiceID
var err error
if result.SenderAddress == nil {
log.Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("No sender address received")
return nil
} else if theirServiceID, err = result.SenderAddress.NameServiceID(); err != nil {
log.Warn().
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("Failed to get sender name as service ID")
return fmt.Errorf("failed to get sender name as service ID: %w", err)
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().
Any("their_service_id", theirServiceID).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("Dropping message from non-ACI sender")
return nil
}
cli.Store.RecipientStore.MarkUnregistered(ctx, theirServiceID, false)
handlerSuccess := true
defer func() {
if retErr == nil && !handlerSuccess {
retErr = ErrHandlerFailed
}
}()
// result.Err is set if there was an error during decryption and we
// should notifiy the user that the message could not be decrypted
if result.Err != nil {
logEvt := log.Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetTimestamp())
if result.SenderAddress == nil {
logEvt.Msg("Decryption error with unknown sender")
return nil
}
theirServiceID, err := result.SenderAddress.NameServiceID()
if err != nil {
log.Err(err).Msg("Name error handling decryption error")
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().Any("their_service_id", theirServiceID).Msg("Sender ServiceID is not an ACI")
}
if errors.Is(result.Err, EventAlreadyProcessed) {
logEvt.Discard().Msg("")
log.Debug().Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Uint64("client_ts", envelope.GetTimestamp()).
Stringer("sender", theirServiceID).
Msg("Ignoring already processed event")
return nil
}
log.Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Stringer("sender", theirServiceID).
Msg("Decryption error with known sender")
logEvt.Stringer("sender", theirServiceID).Msg("Decryption error with known sender")
// Only send decryption error event if the message was urgent,
// to prevent spamming errors for typing notifications and whatnot
if envelope.GetUrgent() &&
@ -489,38 +456,27 @@ func (cli *Client) handleDecryptedResult(
handlerSuccess = cli.handleEvent(&events.DecryptionError{
Sender: theirServiceID.UUID,
Err: result.Err,
Timestamp: envelope.GetClientTimestamp(),
Timestamp: envelope.GetTimestamp(),
})
}
if result.Retriable {
go func() {
err := cli.sendRetryRequest(ctx, result, envelope.GetClientTimestamp())
if err != nil {
log.Err(err).Msg("Failed to send retry request in background")
}
}()
}
if !handlerSuccess {
return ErrHandlerFailed
}
return nil
}
rawContent := result.Content
if rawContent == nil {
content := result.Content
if content == nil {
log.Warn().Msg("Decrypted content is nil")
return nil
}
deviceID, _ := result.SenderAddress.DeviceID()
log.Trace().
Any("raw_data", rawContent).
Stringer("sender", theirServiceID).
Uint("sender_device", deviceID).
Msg("Raw event data")
name, _ := result.SenderAddress.Name()
deviceId, _ := result.SenderAddress.DeviceID()
log.Trace().Any("raw_data", content).Str("sender", name).Uint("sender_device", deviceId).Msg("Raw event data")
newLog := log.With().
Stringer("sender_name", theirServiceID).
Uint("sender_device_id", deviceID).
Str("sender_name", name).
Uint("sender_device_id", deviceId).
Str("destination_service_id", destinationServiceID.String()).
Logger()
log = &newLog
@ -529,32 +485,12 @@ func (cli *Client) handleDecryptedResult(
if result.CiphertextHash != nil {
logEvt = logEvt.Hex("ciphertext_hash", result.CiphertextHash[:])
}
logEvt.Bool("unencrypted", result.Unencrypted).Msg("Decrypted message")
// Handle unencrypted types early and refuse any other unencrypted message
if rawContent.GetDecryptionErrorMessage() != nil {
handlerSuccess = true
dem, err := libsignalgo.DeserializeDecryptionErrorMessage(rawContent.GetDecryptionErrorMessage())
if err != nil {
log.Warn().Err(err).Msg("Failed to unmarshal decryption error message")
} else {
go func() {
err := cli.handleRetryRequest(ctx, result, dem)
if err != nil {
log.Err(err).Msg("Failed to handle decryption error message in background")
}
}()
}
return
} else if result.Unencrypted {
log.Warn().Msg("Unexpected non-decryption-error content in unencrypted message")
return nil
}
logEvt.Msg("Decrypted message")
// If there's a sender key distribution message, process it
if rawContent.SenderKeyDistributionMessage != nil {
if content.GetSenderKeyDistributionMessage() != nil {
log.Debug().Msg("content includes sender key distribution message")
skdm, err := libsignalgo.DeserializeSenderKeyDistributionMessage(rawContent.SenderKeyDistributionMessage)
skdm, err := libsignalgo.DeserializeSenderKeyDistributionMessage(content.GetSenderKeyDistributionMessage())
if err != nil {
log.Err(err).Msg("DeserializeSenderKeyDistributionMessage error")
return err
@ -571,120 +507,214 @@ func (cli *Client) handleDecryptedResult(
}
}
// If we're getting a message to our PNI, mark it as needing a PNI signature message on the next send
theirServiceID, err := result.SenderAddress.NameServiceID()
if err != nil {
log.Err(err).Msg("Name error")
return err
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().Any("their_service_id", theirServiceID).Msg("Sender ServiceID is not an ACI")
return nil
}
if destinationServiceID == cli.Store.PNIServiceID() {
_, err = cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, theirServiceID.UUID, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) {
if recipient.Whitelisted == nil {
log.Debug().Msg("Marking recipient as not whitelisted")
recipient.Whitelisted = ptr.Ptr(false)
changed = true
}
if !recipient.NeedsPNISignature {
log.Debug().Msg("Marking recipient as needing PNI signature")
recipient.NeedsPNISignature = true
changed = true
return true, nil
}
return
return false, nil
})
if err != nil {
log.Err(err).Msg("Failed to set needs_pni_signature flag after receiving message to PNI service ID")
}
}
// If we receive a PNI signature message (because we sent to a PNI earlier), process it
if rawContent.PniSignatureMessage != nil {
if content.GetPniSignatureMessage() != nil {
log.Debug().Msg("Content includes PNI signature message")
err = cli.handlePNISignatureMessage(ctx, theirServiceID, rawContent.PniSignatureMessage)
err = cli.handlePNISignatureMessage(ctx, theirServiceID, content.GetPniSignatureMessage())
if err != nil {
log.Err(err).
Hex("pni_raw", rawContent.PniSignatureMessage.GetPni()).
Hex("pni_raw", content.GetPniSignatureMessage().GetPni()).
Stringer("aci", theirServiceID.UUID).
Msg("Failed to verify ACI-PNI mapping")
}
}
isBlocked, err := cli.Store.RecipientStore.IsBlocked(ctx, theirServiceID.UUID)
if err != nil {
log.Err(err).Stringer("sender", theirServiceID).Msg("Failed to check if sender is blocked")
// TODO: handle more sync messages
if content.SyncMessage != nil {
if content.SyncMessage.Keys != nil {
aep := libsignalgo.AccountEntropyPool(content.SyncMessage.Keys.GetAccountEntropyPool())
cli.Store.MasterKey = content.SyncMessage.Keys.GetMaster()
if aep != "" {
aepMasterKey, err := aep.DeriveSVRKey()
if err != nil {
log.Err(err).Msg("Failed to derive master key from account entropy pool")
} else if cli.Store.MasterKey == nil {
cli.Store.MasterKey = aepMasterKey
log.Debug().Msg("Derived master key from account entropy pool (no master key in sync message)")
} else if !bytes.Equal(aepMasterKey, cli.Store.MasterKey) {
log.Warn().Msg("Derived master key doesn't match one in sync message")
} else {
log.Debug().Msg("Derived master key matches one in sync message")
}
} else {
log.Debug().Msg("No account entropy pool in sync message")
}
err = cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
if err != nil {
log.Err(err).Msg("Failed to save device after receiving master key")
} else {
log.Info().Msg("Received master key")
go cli.SyncStorage(ctx)
}
} else if content.SyncMessage.GetFetchLatest().GetType() == signalpb.SyncMessage_FetchLatest_STORAGE_MANIFEST {
log.Debug().Msg("Received storage manifest fetch latest notice")
go cli.SyncStorage(ctx)
}
syncSent := content.SyncMessage.GetSent()
if syncSent.GetMessage() != nil || syncSent.GetEditMessage() != nil {
destination := syncSent.DestinationServiceId
var syncDestinationServiceID libsignalgo.ServiceID
if destination != nil {
syncDestinationServiceID, err = libsignalgo.ServiceIDFromString(*destination)
if err != nil {
log.Err(err).Msg("Sync message destination parse error")
return err
}
if syncSent.GetDestinationE164() != "" {
aci, pni := syncDestinationServiceID.ToACIAndPNI()
_, err = cli.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, syncSent.GetDestinationE164())
if err != nil {
log.Err(err).Msg("Failed to update recipient E164 after receiving sync message")
}
}
}
if destination == nil && syncSent.GetMessage().GetGroupV2() == nil && syncSent.GetEditMessage().GetDataMessage().GetGroupV2() == nil {
log.Warn().Msg("sync message sent destination is nil")
} else if content.SyncMessage.Sent.Message != nil {
// TODO handle expiration start ts, and maybe the sync message ts?
cli.incomingDataMessage(ctx, content.SyncMessage.Sent.Message, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp())
} else if content.SyncMessage.Sent.EditMessage != nil {
cli.incomingEditMessage(ctx, content.SyncMessage.Sent.EditMessage, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp())
}
}
if content.SyncMessage.Contacts != nil {
log.Debug().Msg("Recieved sync message contacts")
blob := content.SyncMessage.Contacts.Blob
if blob != nil {
contactsBytes, err := DownloadAttachmentWithPointer(ctx, blob, nil)
if err != nil {
log.Err(err).Msg("Contacts Sync DownloadAttachment error")
}
// unmarshall contacts
contacts, avatars, err := unmarshalContactDetailsMessages(contactsBytes)
if err != nil {
log.Err(err).Msg("Contacts Sync unmarshalContactDetailsMessages error")
}
log.Debug().Int("contact_count", len(contacts)).Msg("Contacts Sync received contacts")
convertedContacts := make([]*types.Recipient, 0, len(contacts))
err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error {
for i, signalContact := range contacts {
if signalContact.Aci == nil || *signalContact.Aci == "" {
// TODO lookup PNI via CDSI and store that when ACI is missing?
log.Info().
Any("contact", signalContact).
Msg("Signal Contact UUID is nil, skipping")
continue
}
contact, err := cli.StoreContactDetailsAsContact(ctx, signalContact, &avatars[i])
if err != nil {
return err
}
convertedContacts = append(convertedContacts, contact)
}
return nil
})
if err != nil {
log.Err(err).Msg("Error storing contacts")
} else {
handlerSuccess = cli.handleEvent(&events.ContactList{
Contacts: convertedContacts,
})
}
}
}
if content.SyncMessage.Read != nil {
handlerSuccess = cli.handleEvent(&events.ReadSelf{
Timestamp: envelope.GetTimestamp(),
Messages: content.SyncMessage.GetRead(),
})
}
if content.SyncMessage.DeleteForMe != nil {
handlerSuccess = cli.handleEvent(&events.DeleteForMe{
Timestamp: envelope.GetTimestamp(),
SyncMessage_DeleteForMe: content.SyncMessage.DeleteForMe,
})
}
}
var sendDeliveryReceipt bool
var deliveryReceiptTS uint64
switch content := rawContent.Content.(type) {
case *signalpb.Content_SyncMessage:
if theirServiceID == cli.Store.ACIServiceID() {
handlerSuccess = cli.handleSyncMessage(ctx, content.SyncMessage, envelope)
sendDeliveryReceipt := true
if content.DataMessage != nil {
handlerSuccess = cli.incomingDataMessage(ctx, content.DataMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp())
} else if content.EditMessage != nil {
handlerSuccess = cli.incomingEditMessage(ctx, content.EditMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp())
} else {
sendDeliveryReceipt = false
}
if sendDeliveryReceipt && handlerSuccess {
err = cli.sendDeliveryReceipts(ctx, []uint64{content.DataMessage.GetTimestamp()}, theirServiceID.UUID)
if err != nil {
log.Err(err).Msg("sendDeliveryReceipts error")
}
return nil
case *signalpb.Content_DataMessage:
handlerSuccess, sendDeliveryReceipt = cli.incomingDataMessage(
ctx, content.DataMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp(), isBlocked,
)
deliveryReceiptTS = content.DataMessage.GetTimestamp()
case *signalpb.Content_EditMessage:
handlerSuccess, sendDeliveryReceipt = cli.incomingEditMessage(
ctx, content.EditMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp(), isBlocked,
)
deliveryReceiptTS = content.EditMessage.GetDataMessage().GetTimestamp()
case *signalpb.Content_ReceiptMessage:
if content.ReceiptMessage.GetType() == signalpb.ReceiptMessage_DELIVERY && theirServiceID == cli.Store.ACIServiceID() {
}
if content.TypingMessage != nil {
var groupID types.GroupIdentifier
if content.TypingMessage.GetGroupId() != nil {
gidBytes := content.TypingMessage.GetGroupId()
groupID = types.GroupIdentifier(base64.StdEncoding.EncodeToString(gidBytes))
}
// No handler success check here, nobody cares if typing notifications are dropped
cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: groupOrUserID(groupID, theirServiceID),
ServerTimestamp: envelope.GetServerTimestamp(),
},
Event: content.TypingMessage,
})
}
// DM call message (group call is an opaque callMessage and a groupCallUpdate in a dataMessage)
if content.CallMessage != nil && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) {
handlerSuccess = cli.handleEvent(&events.Call{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: theirServiceID.String(),
ServerTimestamp: envelope.GetServerTimestamp(),
},
// CallMessage doesn't have its own timestamp, use one from the envelope
Timestamp: envelope.GetTimestamp(),
IsRinging: content.CallMessage.Offer != nil,
}) && handlerSuccess
}
// Read and delivery receipts
if content.ReceiptMessage != nil {
if content.GetReceiptMessage().GetType() == signalpb.ReceiptMessage_DELIVERY && theirServiceID == cli.Store.ACIServiceID() {
// Ignore delivery receipts from other own devices
return nil
}
handlerSuccess = cli.handleEvent(&events.Receipt{
Sender: theirServiceID.UUID,
Content: content.ReceiptMessage,
})
case *signalpb.Content_TypingMessage:
var groupID types.GroupIdentifier
if content.TypingMessage.GetGroupId() != nil {
gidBytes := content.TypingMessage.GetGroupId()
groupID = types.GroupIdentifier(base64.StdEncoding.EncodeToString(gidBytes))
}
if !isBlocked || groupID != "" {
// No handler success check here, nobody cares if typing notifications are dropped
cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: groupOrUserID(groupID, theirServiceID),
ServerTimestamp: envelope.GetServerTimestamp(),
},
Event: content.TypingMessage,
})
}
case *signalpb.Content_CallMessage:
if !isBlocked && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) {
handlerSuccess = cli.handleEvent(&events.Call{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: theirServiceID.String(),
ServerTimestamp: envelope.GetServerTimestamp(),
},
// CallMessage doesn't have its own timestamp, use one from the envelope
Timestamp: envelope.GetClientTimestamp(),
IsRinging: content.CallMessage.Offer != nil,
})
}
case *signalpb.Content_DecryptionErrorMessage:
// These should've been handled earlier
log.Warn().Msg("Unexpected decryption error message content in decrypted message")
case *signalpb.Content_NullMessage:
// This is intentionally ignored
case *signalpb.Content_StoryMessage:
// This is also ignored for now
default:
if rawContent.PniSignatureMessage == nil && rawContent.SenderKeyDistributionMessage == nil {
log.Warn().Type("content_type", content).Msg("Unrecognized message content type")
}
}) && handlerSuccess
}
if sendDeliveryReceipt && handlerSuccess {
err = cli.sendDeliveryReceipts(ctx, []uint64{deliveryReceiptTS}, theirServiceID.UUID)
if err != nil {
log.Err(err).Msg("sendDeliveryReceipts error")
}
if !handlerSuccess {
return ErrHandlerFailed
}
return nil
}
@ -695,184 +725,6 @@ func groupOrUserID(groupID types.GroupIdentifier, userID libsignalgo.ServiceID)
return string(groupID)
}
func (cli *Client) handleSyncMessage(ctx context.Context, msg *signalpb.SyncMessage, envelope *signalpb.Envelope) (handlerSuccess bool) {
// TODO: handle more sync messages
handlerSuccess = true
log := zerolog.Ctx(ctx)
switch content := msg.Content.(type) {
case *signalpb.SyncMessage_Keys_:
aep := libsignalgo.AccountEntropyPool(content.Keys.GetAccountEntropyPool())
if aep != "" {
aepMasterKey, err := aep.DeriveSVRKey()
if err != nil {
log.Err(err).Msg("Failed to derive master key from account entropy pool")
} else if cli.Store.MasterKey == nil {
cli.Store.MasterKey = aepMasterKey
log.Debug().Msg("Derived master key from account entropy pool (no master key in sync message)")
} else if !bytes.Equal(aepMasterKey, cli.Store.MasterKey) {
log.Warn().Msg("Derived master key doesn't match one in sync message")
} else {
log.Debug().Msg("Derived master key matches one in sync message")
}
} else {
log.Debug().Msg("No account entropy pool in sync message")
}
err := cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
if err != nil {
log.Err(err).Msg("Failed to save device after receiving master key")
} else {
log.Info().Msg("Received master key")
go cli.SyncStorage(ctx)
}
case *signalpb.SyncMessage_FetchLatest_:
switch content.FetchLatest.GetType() {
case signalpb.SyncMessage_FetchLatest_STORAGE_MANIFEST:
log.Debug().Msg("Received storage manifest fetch latest notice")
go cli.SyncStorage(ctx)
default:
log.Debug().
Stringer("fetch_latest_type", content.FetchLatest.GetType()).
Msg("Received unknown fetch latest notice")
}
case *signalpb.SyncMessage_Sent_:
syncSent := content.Sent
if syncSent.GetMessage() != nil || syncSent.GetEditMessage() != nil {
syncDestinationServiceID, err := ParseStringOrBinaryServiceID(syncSent.GetDestinationServiceId(), syncSent.GetDestinationServiceIdBinary())
if err != nil && !errors.Is(err, ErrEmptyUUIDInput) {
log.Err(err).Msg("Sync message destination parse error")
}
if syncSent.GetDestinationE164() != "" && !syncDestinationServiceID.IsEmpty() {
aci, pni := syncDestinationServiceID.ToACIAndPNI()
_, err = cli.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, syncSent.GetDestinationE164())
if err != nil {
log.Err(err).Msg("Failed to update recipient E164 after receiving sync message")
}
}
for _, unident := range syncSent.GetUnidentifiedStatus() {
serviceID, err := ParseStringOrBinaryServiceID(unident.GetDestinationServiceId(), unident.GetDestinationServiceIdBinary())
if err != nil {
log.Err(err).
Str("destination_service_id", unident.GetDestinationServiceId()).
Hex("destination_service_id_bytes", unident.GetDestinationServiceIdBinary()).
Msg("Failed to parse destination service ID of unidentified send")
continue
}
changed, err := cli.saveSyncPNIIdentityKey(ctx, serviceID, unident.GetDestinationPniIdentityKey())
if err != nil {
log.Err(err).
Stringer("destination_service_id", serviceID).
Msg("Failed to save PNI identity key from sync message")
} else if changed {
log.Debug().
Stringer("destination_service_id", serviceID).
Msg("Saved new PNI identity key from sync message")
}
}
if syncDestinationServiceID.IsEmpty() && syncSent.GetMessage().GetGroupV2() == nil && syncSent.GetEditMessage().GetDataMessage().GetGroupV2() == nil {
log.Warn().Msg("sync message sent destination is nil")
} else if syncSent.Message != nil {
// TODO handle expiration start ts, and maybe the sync message ts?
cli.incomingDataMessage(ctx, syncSent.Message, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp(), false)
} else if syncSent.EditMessage != nil {
cli.incomingEditMessage(ctx, syncSent.EditMessage, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp(), false)
}
}
case *signalpb.SyncMessage_Contacts_:
log.Debug().Msg("Recieved sync message contacts")
if content.Contacts.Blob != nil {
// TODO roundtrip via disk to save memory
contactsBytes, err := DownloadAttachmentWithPointer(ctx, content.Contacts.Blob, nil, nil)
if err != nil {
log.Err(err).Msg("Contacts Sync DownloadAttachment error")
}
// unmarshall contacts
contacts, avatars, err := unmarshalContactDetailsMessages(contactsBytes)
if err != nil {
log.Err(err).Msg("Contacts Sync unmarshalContactDetailsMessages error")
}
log.Debug().Int("contact_count", len(contacts)).Msg("Contacts Sync received contacts")
convertedContacts := make([]*types.Recipient, 0, len(contacts))
err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error {
for i, signalContact := range contacts {
if (signalContact.Aci == nil || *signalContact.Aci == "") && len(signalContact.AciBinary) != 16 {
// TODO lookup PNI via CDSI and store that when ACI is missing?
log.Info().
Any("contact", signalContact).
Msg("Signal Contact UUID is nil, skipping")
continue
}
contact, err := cli.StoreContactDetailsAsContact(ctx, signalContact, &avatars[i])
if err != nil {
return err
}
convertedContacts = append(convertedContacts, contact)
}
return nil
})
if err != nil {
log.Err(err).Msg("Error storing contacts")
} else {
handlerSuccess = cli.handleEvent(&events.ContactList{
Contacts: convertedContacts,
})
}
}
case *signalpb.SyncMessage_DeleteForMe_:
handlerSuccess = cli.handleEvent(&events.DeleteForMe{
Timestamp: envelope.GetClientTimestamp(),
SyncMessage_DeleteForMe: content.DeleteForMe,
})
case *signalpb.SyncMessage_MessageRequestResponse_:
aciUUID, _ := ParseStringOrBinaryUUID(content.MessageRequestResponse.GetThreadAci(), content.MessageRequestResponse.GetThreadAciBinary())
if aciUUID != uuid.Nil && content.MessageRequestResponse.GetType() == signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
_, err := cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aciUUID, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) {
changed = !ptr.Val(recipient.Whitelisted) || recipient.NeedsPNISignature
recipient.Whitelisted = ptr.Ptr(true)
recipient.NeedsPNISignature = false
return
})
if err != nil {
log.Err(err).Msg("Failed to clear needs_pni_signature flag after message request accept")
}
}
var groupID *libsignalgo.GroupIdentifier
if len(content.MessageRequestResponse.GroupId) == libsignalgo.GroupIdentifierLength {
groupID = (*libsignalgo.GroupIdentifier)(content.MessageRequestResponse.GroupId)
}
handlerSuccess = cli.handleEvent(&events.MessageRequestResponse{
Timestamp: envelope.GetClientTimestamp(),
ThreadACI: aciUUID,
GroupID: groupID,
Type: content.MessageRequestResponse.GetType(),
Raw: content.MessageRequestResponse,
})
default:
if msg.Read != nil {
handlerSuccess = cli.handleEvent(&events.ReadSelf{
Timestamp: envelope.GetClientTimestamp(),
Messages: msg.Read,
})
}
}
return
}
func (cli *Client) saveSyncPNIIdentityKey(ctx context.Context, serviceID libsignalgo.ServiceID, identityKeyBytes []byte) (bool, error) {
if identityKeyBytes == nil || serviceID.Type != libsignalgo.ServiceIDTypePNI {
return false, nil
}
identityKey, err := libsignalgo.DeserializeIdentityKey(identityKeyBytes)
if err != nil {
return false, fmt.Errorf("failed to deserialize PNI identity key: %w", err)
}
changed, err := cli.Store.IdentityKeyStore.SaveIdentityKey(ctx, serviceID, identityKey)
if err != nil {
return false, fmt.Errorf("failed to save PNI identity key: %w", err)
}
return changed, nil
}
func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsignalgo.ServiceID, msg *signalpb.PniSignatureMessage) error {
if sender.Type != libsignalgo.ServiceIDTypeACI {
return fmt.Errorf("PNI signature message sender is not an ACI")
@ -923,14 +775,7 @@ func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsign
return nil
}
func (cli *Client) incomingEditMessage(
ctx context.Context,
editMessage *signalpb.EditMessage,
messageSenderACI uuid.UUID,
chatRecipient libsignalgo.ServiceID,
serverTimestamp uint64,
isBlocked bool,
) (handlerSuccess, sendDeliveryReceipt bool) {
func (cli *Client) incomingEditMessage(ctx context.Context, editMessage *signalpb.EditMessage, messageSenderACI uuid.UUID, chatRecipient libsignalgo.ServiceID, serverTimestamp uint64) bool {
// If it's a group message, get the ID and invalidate cache if necessary
var groupID types.GroupIdentifier
var groupRevision uint32
@ -942,12 +787,9 @@ func (cli *Client) incomingEditMessage(
groupID, err = cli.StoreMasterKey(ctx, masterKey)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error")
return
return false
}
groupRevision = editMessage.GetDataMessage().GetGroupV2().GetRevision()
} else if isBlocked {
zerolog.Ctx(ctx).Debug().Msg("Dropping direct message from blocked user")
return true, false
}
return cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{
@ -957,24 +799,17 @@ func (cli *Client) incomingEditMessage(
ServerTimestamp: serverTimestamp,
},
Event: editMessage,
}), true
})
}
func (cli *Client) incomingDataMessage(
ctx context.Context,
dataMessage *signalpb.DataMessage,
messageSenderACI uuid.UUID,
chatRecipient libsignalgo.ServiceID,
serverTimestamp uint64,
isBlocked bool,
) (handlerSuccess, sendDeliveryReceipt bool) {
func (cli *Client) incomingDataMessage(ctx context.Context, dataMessage *signalpb.DataMessage, messageSenderACI uuid.UUID, chatRecipient libsignalgo.ServiceID, serverTimestamp uint64) bool {
// If there's a profile key, save it
if dataMessage.ProfileKey != nil {
profileKey := libsignalgo.ProfileKey(dataMessage.ProfileKey)
err := cli.Store.RecipientStore.StoreProfileKey(ctx, messageSenderACI, profileKey)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreProfileKey error")
return
return false
}
}
@ -989,12 +824,9 @@ func (cli *Client) incomingDataMessage(
groupID, err = cli.StoreMasterKey(ctx, masterKey)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error")
return
return false
}
groupRevision = dataMessage.GetGroupV2().GetRevision()
} else if isBlocked {
zerolog.Ctx(ctx).Debug().Msg("Dropping direct message from blocked user")
return true, false
}
evtInfo := events.MessageInfo{
@ -1005,17 +837,17 @@ func (cli *Client) incomingDataMessage(
}
// Hacky special case for group calls to cache the state
if dataMessage.GroupCallUpdate != nil {
isRinging := cli.GroupCache.UpdateActiveCall(groupID, dataMessage.GroupCallUpdate.GetEraId())
isRinging := cli.UpdateActiveCalls(groupID, dataMessage.GroupCallUpdate.GetEraId())
return cli.handleEvent(&events.Call{
Info: evtInfo,
Timestamp: dataMessage.GetTimestamp(),
IsRinging: isRinging,
}), true
})
} else {
return cli.handleEvent(&events.ChatEvent{
Info: evtInfo,
Event: dataMessage,
}), true
})
}
}

View file

@ -39,21 +39,18 @@ type DecryptionResult struct {
Content *signalpb.Content
ContentHint signalpb.UnidentifiedSenderMessage_Message_ContentHint
Err error
GroupID *libsignalgo.GroupIdentifier
Unencrypted bool
Retriable bool
Ciphertext []byte
CiphertextType libsignalgo.CiphertextMessageType
}
func (cli *Client) decryptEnvelope(
ctx context.Context,
envelope *signalpb.Envelope,
sourceServiceID, destinationServiceID libsignalgo.ServiceID,
) DecryptionResult {
if destinationServiceID.IsEmpty() {
return DecryptionResult{Err: fmt.Errorf("envelope missing destination service ID")}
log := zerolog.Ctx(ctx)
destinationServiceID, err := libsignalgo.ServiceIDFromString(envelope.GetDestinationServiceId())
if err != nil {
log.Err(err).Str("destination_service_id", envelope.GetDestinationServiceId()).Msg("Failed to parse destination service ID")
return DecryptionResult{Err: fmt.Errorf("failed to parse destination service ID: %w", err)}
}
switch *envelope.Type {
@ -64,14 +61,17 @@ func (cli *Client) decryptEnvelope(
}
return result
case signalpb.Envelope_PREKEY_MESSAGE, signalpb.Envelope_DOUBLE_RATCHET:
sender, err := sourceServiceID.Address(uint(envelope.GetSourceDeviceId()))
case signalpb.Envelope_PREKEY_BUNDLE, signalpb.Envelope_CIPHERTEXT:
sender, err := libsignalgo.NewUUIDAddressFromString(
*envelope.SourceServiceId,
uint(*envelope.SourceDevice),
)
if err != nil {
return DecryptionResult{Err: fmt.Errorf("failed to wrap address: %v", err)}
}
var result *DecryptionResult
var bundleType string
if *envelope.Type == signalpb.Envelope_PREKEY_MESSAGE {
if *envelope.Type == signalpb.Envelope_PREKEY_BUNDLE {
result, err = cli.prekeyDecrypt(ctx, destinationServiceID, sender, envelope.Content, envelope.GetServerTimestamp())
bundleType = "prekey bundle"
} else {
@ -79,34 +79,19 @@ func (cli *Client) decryptEnvelope(
bundleType = "ciphertext"
}
if err != nil {
return DecryptionResult{
SenderAddress: sender,
Err: fmt.Errorf("failed to decrypt %s envelope: %w", bundleType, err),
Retriable: true, // TODO should these ever be not retriable?
Ciphertext: envelope.Content,
CiphertextType: libsignalgo.CiphertextMessageType(envelope.GetType()),
}
return DecryptionResult{Err: fmt.Errorf("failed to decrypt %s envelope: %w", bundleType, err), SenderAddress: sender}
}
return *result
case signalpb.Envelope_PLAINTEXT_CONTENT:
addr, err := sourceServiceID.Address(uint(envelope.GetSourceDeviceId()))
if err != nil {
return DecryptionResult{Err: fmt.Errorf("failed to wrap address: %v", err)}
}
content, err := stripPadding(envelope.GetContent())
if err != nil {
return DecryptionResult{Err: fmt.Errorf("failed to strip padding: %w", err)}
}
return DecryptionResult{
SenderAddress: addr,
Content: &signalpb.Content{Content: &signalpb.Content_DecryptionErrorMessage{DecryptionErrorMessage: content}},
Unencrypted: true,
}
return DecryptionResult{Err: fmt.Errorf("plaintext messages are not supported")}
case signalpb.Envelope_SERVER_DELIVERY_RECEIPT:
return DecryptionResult{Err: fmt.Errorf("server delivery receipt envelopes are not yet supported")}
case signalpb.Envelope_SENDERKEY_MESSAGE:
return DecryptionResult{Err: fmt.Errorf("senderkey message envelopes are not yet supported")}
case signalpb.Envelope_UNKNOWN:
return DecryptionResult{Err: fmt.Errorf("unknown envelope type")}
@ -185,17 +170,12 @@ func (cli *Client) prekeyDecrypt(
if is == nil {
return nil, fmt.Errorf("no identity store found for %s", destination)
}
destinationAddress, err := destination.Address(uint(cli.Store.DeviceID))
if err != nil {
return nil, fmt.Errorf("failed to get own/destination address: %w", err)
}
plaintext, ciphertextHash, err := cli.bufferedDecryptTxn(ctx, encryptedContent, serverTimestamp, func(ctx context.Context) ([]byte, error) {
return libsignalgo.DecryptPreKey(
ctx,
preKeyMessage,
sender,
destinationAddress,
ss,
is,
pks,
@ -243,16 +223,11 @@ func (cli *Client) decryptCiphertextEnvelope(
if identityStore == nil {
return nil, fmt.Errorf("no identity store for destination service ID %s", destinationServiceID)
}
destinationAddress, err := destinationServiceID.Address(uint(cli.Store.DeviceID))
if err != nil {
return nil, fmt.Errorf("failed to get own address: %w", err)
}
plaintext, ciphertextHash, err := cli.bufferedDecryptTxn(ctx, ciphertext, serverTimestamp, func(ctx context.Context) ([]byte, error) {
return libsignalgo.Decrypt(
ctx,
message,
senderAddress,
destinationAddress,
sessionStore,
identityStore,
)
@ -340,10 +315,6 @@ func (cli *Client) decryptUnidentifiedSenderEnvelope(ctx context.Context, destin
if err != nil {
return result, fmt.Errorf("failed to get content hint: %w", err)
}
result.GroupID, err = usmc.GetGroupID()
if err != nil {
return result, fmt.Errorf("failed to get group ID: %w", err)
}
result.ContentHint = signalpb.UnidentifiedSenderMessage_Message_ContentHint(contentHint)
senderUUID, err := senderCertificate.GetSenderUUID()
if err != nil {
@ -366,11 +337,8 @@ func (cli *Client) decryptUnidentifiedSenderEnvelope(ctx context.Context, destin
if err != nil {
return result, fmt.Errorf("failed to get USMC contents: %w", err)
}
result.Ciphertext = usmcContents
result.CiphertextType = messageType
newLog := log.With().
Stringer("sender_uuid", senderUUID).
Stringer("group_id", result.GroupID).
Uint32("sender_device_id", senderDeviceID).
Str("sender_e164", senderE164).
Uint8("sealed_sender_type", uint8(messageType)).
@ -395,25 +363,15 @@ func (cli *Client) decryptUnidentifiedSenderEnvelope(ctx context.Context, destin
case libsignalgo.CiphertextMessageTypeWhisper:
resultPtr, err = cli.decryptCiphertextEnvelope(ctx, destinationServiceID, senderAddress, usmcContents, envelope.GetServerTimestamp())
case libsignalgo.CiphertextMessageTypePlaintext:
usmcContents, err = stripPadding(usmcContents)
if err != nil {
err = fmt.Errorf("failed to strip padding: %w", err)
}
result.Unencrypted = true
result.Content = &signalpb.Content{
Content: &signalpb.Content_DecryptionErrorMessage{
DecryptionErrorMessage: usmcContents,
},
}
return result, err
// TODO: handle plaintext (usually DecryptionErrorMessage) and retries
// when implementing SenderKey groups
return result, fmt.Errorf("unsupported plaintext sealed sender message")
default:
return result, fmt.Errorf("unsupported sealed sender message type %d", messageType)
}
if err != nil {
result.Retriable = result.ContentHint == signalpb.UnidentifiedSenderMessage_Message_RESENDABLE
return result, err
}
resultPtr.GroupID = result.GroupID
return *resultPtr, 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 signalmeow
import (
"context"
"fmt"
"math/rand/v2"
"slices"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/random"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
type sendCacheKey struct {
recipient libsignalgo.ServiceID
groupID types.GroupIdentifier
timestamp uint64
}
const RetryRespondMaxAge = 30 * 24 * time.Hour
func (cli *Client) sendRetryRequest(ctx context.Context, result DecryptionResult, originalTS uint64) error {
serviceID, err := result.SenderAddress.NameServiceID()
if err != nil {
return fmt.Errorf("failed to get sender name as service ID: %w", err)
}
deviceID, err := result.SenderAddress.DeviceID()
if err != nil {
return fmt.Errorf("failed to get sender device ID: %w", err)
}
dem, err := libsignalgo.DecryptionErrorMessageForOriginalMessage(result.Ciphertext, result.CiphertextType, originalTS, deviceID)
if err != nil {
return fmt.Errorf("failed to create decryption error message: %w", err)
}
demBytes, err := dem.Serialize()
if err != nil {
return fmt.Errorf("failed to serialize decryption error message: %w", err)
}
ptc, err := libsignalgo.PlaintextContentFromDecryptionErrorMessage(dem)
if err != nil {
return fmt.Errorf("failed to create plaintext content from decryption error message: %w", err)
}
ctm, err := libsignalgo.NewCiphertextMessage(ptc)
if err != nil {
return fmt.Errorf("failed to create ciphertext message from plaintext content: %w", err)
}
_, err = cli.sendContent(ctx, serviceID, uint64(time.Now().UnixMilli()), &signalpb.Content{
Content: &signalpb.Content_DecryptionErrorMessage{
DecryptionErrorMessage: demBytes,
},
}, 0, true, result.GroupID, ctm)
if err != nil {
return fmt.Errorf("failed to send decryption error message: %w", err)
}
zerolog.Ctx(ctx).Debug().
Stringer("sender_service_id", serviceID).
Uint("sender_device_id", deviceID).
Stringer("group_id", result.GroupID).
Msg("Sent retry receipt")
return nil
}
func (cli *Client) handleRetryRequest(
ctx context.Context,
result DecryptionResult,
dem *libsignalgo.DecryptionErrorMessage,
) error {
destDeviceID, err := dem.GetDeviceID()
if err != nil {
return fmt.Errorf("failed to get device ID from decryption error message: %w", err)
} else if int(destDeviceID) != cli.Store.DeviceID {
zerolog.Ctx(ctx).Debug().
Uint32("dest_device_id", destDeviceID).
Msg("Ignoring decryption error message for another device")
return nil
}
serviceID, err := result.SenderAddress.NameServiceID()
if err != nil {
return fmt.Errorf("failed to get sender name as service ID: %w", err)
}
deviceID, err := result.SenderAddress.DeviceID()
if err != nil {
return fmt.Errorf("failed to get sender device ID: %w", err)
}
ts, err := dem.GetTimestamp()
if err != nil {
return fmt.Errorf("failed to get timestamp: %w", err)
}
cli.encryptionLock.Lock()
defer cli.encryptionLock.Unlock()
ctx = context.WithValue(ctx, contextKeyEncryptionLock, true)
var didArchiveSession bool
if ratchetKey, err := dem.GetRatchetKey(); err != nil {
return fmt.Errorf("failed to get ratchet key: %w", err)
} else if ratchetKey == nil {
// No need to archive session if no ratchet key is provided, it was probably a sender key decryption error
} else if session, err := cli.Store.ACISessionStore.LoadSession(ctx, result.SenderAddress); err != nil {
return fmt.Errorf("failed to load session for sender: %w", err)
} else if match, err := session.CurrentRatchetKeyMatches(ratchetKey); err != nil {
return fmt.Errorf("failed to check ratchet key match: %w", err)
} else if match {
err = session.ArchiveCurrentState()
if err != nil {
return fmt.Errorf("failed to archive current session state: %w", err)
}
err = cli.Store.ACISessionStore.StoreSession(ctx, result.SenderAddress, session)
if err != nil {
return fmt.Errorf("failed to store archived session: %w", err)
}
didArchiveSession = true
}
var skdmBytes []byte
groupID := types.BytesToGroupIdentifier(result.GroupID)
if groupID != "" {
ski, err := cli.Store.SenderKeyStore.GetSenderKeyInfo(ctx, groupID)
if err != nil {
return fmt.Errorf("failed to get sender key info for group %s: %w", groupID, err)
}
myAddress, err := cli.Store.ACIServiceID().Address(uint(cli.Store.DeviceID))
if err != nil {
return fmt.Errorf("failed to get own address: %w", err)
}
if slices.Contains(ski.SharedWith[serviceID], int(deviceID)) {
skdm, err := libsignalgo.NewSenderKeyDistributionMessage(ctx, myAddress, ski.DistributionID, cli.Store.SenderKeyStore)
if err != nil {
return fmt.Errorf("failed to create sender key distribution message: %w", err)
}
skdmBytes, err = skdm.Serialize()
if err != nil {
return fmt.Errorf("failed to serialize sender key distribution message: %w", err)
}
} else {
zerolog.Ctx(ctx).Warn().
Stringer("group_id", result.GroupID).
Stringer("sender_service_id", serviceID).
Stringer("distribution_id", ski.DistributionID).
Uint("sender_device_id", deviceID).
Ints("shared_with", ski.SharedWith[serviceID]).
Msg("Sender key distribution list doesn't contain retry receipt sender")
}
}
var retryContent *signalpb.Content
var cacheHit bool
if time.Since(time.UnixMilli(int64(ts))) < RetryRespondMaxAge {
retryContent, cacheHit = cli.sendCache.Get(sendCacheKey{
groupID: groupID,
recipient: serviceID,
timestamp: ts,
})
if !cacheHit {
// TODO add support for external caches
}
}
if retryContent == nil {
retryContent = &signalpb.Content{}
}
retryContent.SenderKeyDistributionMessage = skdmBytes
if !cacheHit && skdmBytes == nil {
if !didArchiveSession {
zerolog.Ctx(ctx).Debug().
Uint64("msg_timestamp", ts).
Stringer("sender_service_id", serviceID).
Uint("sender_device_id", deviceID).
Stringer("group_id", result.GroupID).
Msg("Not responding to decryption error message")
return nil
}
retryContent.Content = &signalpb.Content_NullMessage{
NullMessage: &signalpb.NullMessage{
Padding: random.Bytes(rand.IntN(511) + 1),
},
}
}
responseTimestamp := uint64(time.Now().UnixMilli())
if cacheHit {
responseTimestamp = ts
}
zerolog.Ctx(ctx).Debug().
Uint32("dest_device_id", destDeviceID).
Uint64("requested_msg_timestamp", ts).
Stringer("sender_service_id", serviceID).
Uint("sender_device_id", deviceID).
Stringer("group_id", result.GroupID).
Bool("did_archive_session", didArchiveSession).
Bool("found_message_in_cache", cacheHit).
Bool("including_skdm", skdmBytes != nil).
Msg("Responding to decryption error message")
_, err = cli.sendContent(ctx, serviceID, responseTimestamp, retryContent, 0, true, result.GroupID, nil)
if err != nil {
return fmt.Errorf("failed to send response: %w", err)
}
return nil
}

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