mirror of
https://github.com/mautrix/signal.git
synced 2026-08-23 12:24:56 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ff478b1f |
146 changed files with 7558 additions and 17996 deletions
14
.github/ISSUE_TEMPLATE/bug.md
vendored
14
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -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.
|
||||
-->
|
||||
|
|
|
|||
16
.github/workflows/go.yml
vendored
16
.github/workflows/go.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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$"
|
||||
|
|
|
|||
87
CHANGELOG.md
87
CHANGELOG.md
|
|
@ -1,93 +1,8 @@
|
|||
# v26.08
|
||||
# v25.11 (unreleased)
|
||||
|
||||
* Updated libsignal to v0.100.0
|
||||
* Added support for handling own profile key changes.
|
||||
* Changed attachment bridging behavior to match Signal Desktop.
|
||||
* Fixed edited messages being bridged twice if certain race conditions occurred.
|
||||
|
||||
# v26.07
|
||||
|
||||
* Updated Docker image to Alpine 3.24.
|
||||
* Updated libsignal to v0.97.2.
|
||||
* Added automatic retry when sender key send fails due to missing session.
|
||||
* Fixed changing poll votes from Matrix.
|
||||
|
||||
# v26.06
|
||||
|
||||
* Updated libsignal to v0.94.4.
|
||||
* Updated mrenclave to fix looking up phone numbers.
|
||||
* Changed knock accept handling to auto-join the ghost user afterwards.
|
||||
|
||||
# v26.05
|
||||
|
||||
* Updated libsignal to v0.93.2.
|
||||
* Added support for importing sticker packs from Signal.
|
||||
|
||||
# v26.04
|
||||
|
||||
* Updated libsignal to v0.92.1.
|
||||
* Added support for admin message deletes from Signal.
|
||||
* Added support for binary service IDs in storage service.
|
||||
* Fixed `private_chat_portal_meta` option not setting DM room names correctly.
|
||||
* Fixed panic if user is logged out during initial chat sync.
|
||||
* Fixed avatar upload failing when creating new Signal group.
|
||||
|
||||
# v26.03
|
||||
|
||||
* Switched to sending binary service ID fields in outgoing messages.
|
||||
* Added support for roundtripping large attachments via disk to avoid keeping
|
||||
the entire file in memory during en/decryption.
|
||||
|
||||
# v26.02.2
|
||||
|
||||
* Added support for more new binary service ID fields.
|
||||
|
||||
# v26.02.1
|
||||
|
||||
* Updated libsignal to v0.87.5.
|
||||
* Added support for new binary service ID fields that Signal 8.0 switched to.
|
||||
|
||||
# v26.02
|
||||
|
||||
* Bumped minimum Go version to 1.25.
|
||||
* Updated libsignal to v0.87.1.
|
||||
* Added automatic recovery for the session not found error from libsignal.
|
||||
* Fixed sender key state not being cleared on logout properly.
|
||||
|
||||
# v26.01
|
||||
|
||||
* Updated libsignal to v0.86.12.
|
||||
* Changed automatic contact list sync option to only sync every 3 days rather
|
||||
than on every restart.
|
||||
* Fixed sending messages to groups with no other registered members.
|
||||
* Fixed sender key sends failing if some users had changed devices.
|
||||
* Fixed timestamps of outgoing typing notifications in DMs.
|
||||
|
||||
# v25.12
|
||||
|
||||
* Updated libsignal to v0.86.8.
|
||||
* Updated Docker image to Alpine 3.23.
|
||||
* Added support for dropping incoming DMs from blocked contacts on Signal.
|
||||
* Added support for sender key encryption when sending to groups, which makes
|
||||
sending much faster and enables sending typing notifications.
|
||||
* Added support for encryption retry receipts.
|
||||
* Fixed bugs with handling poll votes.
|
||||
* Fixed history transfer option not showing up when pairing with Signal Android.
|
||||
* Fixed nicknames being cleared not being bridged
|
||||
(thanks to [@Enzime] in [#623]).
|
||||
|
||||
[#623]: https://github.com/mautrix/signal/pull/623
|
||||
[@Enzime]: https://github.com/Enzime
|
||||
|
||||
# v25.11
|
||||
|
||||
* Updated libsignal to v0.86.4.
|
||||
* Added support for bridging invite state in groups for phone number invites.
|
||||
* Added support for polls.
|
||||
* Fixed PNI signature not being sent when replying to message requests.
|
||||
* Fixed unnecessary repeating error notices when Signal is down.
|
||||
* Fixed sticker size metadata on Matrix not matching how native Signal Desktop
|
||||
renders them.
|
||||
|
||||
# v25.10
|
||||
|
||||
|
|
|
|||
22
Dockerfile
22
Dockerfile
|
|
@ -1,17 +1,18 @@
|
|||
# -- Build libsignal (with Rust) --
|
||||
FROM rust:1-alpine AS rust-builder
|
||||
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev protobuf-dev
|
||||
FROM rust:1-alpine as rust-builder
|
||||
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
|
||||
|
||||
WORKDIR /build
|
||||
# Copy all files needed for Rust build, and no Go files
|
||||
COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
|
||||
COPY build-rust.sh .
|
||||
|
||||
ARG DBG=0
|
||||
RUN ./build-rust.sh
|
||||
|
||||
# -- Build mautrix-signal (with Go) --
|
||||
FROM golang:1-alpine3.24 AS go-builder
|
||||
RUN apk add --no-cache git ca-certificates build-base olm-dev zlib-dev
|
||||
FROM golang:1-alpine3.22 AS go-builder
|
||||
RUN apk add --no-cache git ca-certificates build-base olm-dev
|
||||
|
||||
WORKDIR /build
|
||||
# Copy all files needed for Go build, and no Rust files
|
||||
|
|
@ -25,14 +26,20 @@ COPY pkg/connector/. pkg/connector/.
|
|||
COPY cmd/. cmd/.
|
||||
COPY .git .git
|
||||
|
||||
ARG DBG=0
|
||||
ENV LIBRARY_PATH=.
|
||||
COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./
|
||||
RUN <<EOF
|
||||
if [ "$DBG" = 1 ]; then
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
else
|
||||
touch /go/bin/dlv
|
||||
fi
|
||||
EOF
|
||||
RUN ./build-go.sh
|
||||
|
||||
# -- Run mautrix-signal --
|
||||
FROM alpine:3.24
|
||||
FROM alpine:3.22
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
@ -41,6 +48,11 @@ RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go olm
|
|||
|
||||
COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal
|
||||
COPY --from=go-builder /build/docker-run.sh /docker-run.sh
|
||||
COPY --from=go-builder /go/bin/dlv /usr/bin/dlv
|
||||
VOLUME /data
|
||||
|
||||
ARG DBG
|
||||
ARG DBGWAIT=0
|
||||
ENV DBG=${DBG} DBGWAIT=${DBGWAIT}
|
||||
RUN echo "Debug mode: DBG=${DBG} DBGWAIT=${DBGWAIT}"
|
||||
CMD ["/docker-run.sh"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
ARG DOCKER_HUB="docker.io"
|
||||
|
||||
FROM ${DOCKER_HUB}/alpine:3.24
|
||||
FROM ${DOCKER_HUB}/alpine:3.22
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
build.sh
1
build.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -17,12 +17,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/connector"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
||||
)
|
||||
|
||||
// Information to find out exactly which commit the bridge was built from.
|
||||
|
|
@ -37,14 +34,13 @@ var m = mxmain.BridgeMain{
|
|||
Name: "mautrix-signal",
|
||||
URL: "https://github.com/mautrix/signal",
|
||||
Description: "A Matrix-Signal puppeting bridge.",
|
||||
Version: "26.08",
|
||||
Version: "25.10",
|
||||
SemCalVer: true,
|
||||
|
||||
Connector: &connector.SignalConnector{},
|
||||
}
|
||||
|
||||
func main() {
|
||||
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
|
||||
m.PostStart = func() {
|
||||
if m.Matrix.Provisioning != nil {
|
||||
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)
|
||||
|
|
|
|||
45
go.mod
45
go.mod
|
|
@ -1,51 +1,48 @@
|
|||
module go.mau.fi/mautrix-signal
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.26.6
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
toolchain go1.25.3
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-pointer v0.0.1
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
go.mau.fi/util v0.10.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.mau.fi/util v0.9.2
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b
|
||||
golang.org/x/net v0.46.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.30.0
|
||||
maunium.net/go/mautrix v0.25.3-0.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.49 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/yuin/goldmark v1.8.5 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
|
|
|
|||
84
go.sum
84
go.sum
|
|
@ -1,16 +1,17 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
|
@ -22,19 +23,23 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
|
||||
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
||||
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
|
||||
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
|
|
@ -42,46 +47,45 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
|||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
|
||||
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM=
|
||||
go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU=
|
||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -91,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
|
||||
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=
|
||||
maunium.net/go/mautrix v0.25.3-0.20251027130059-9f2669025f28 h1:BG0IPWqlVR2mxAQM8jhOhHa168si5iXKeTv11nCGj58=
|
||||
maunium.net/go/mautrix v0.25.3-0.20251027130059-9f2669025f28/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI=
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
|||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.signal.capabilities.2026_07_22"
|
||||
base := "fi.mau.signal.capabilities.2025_10_27"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@ var signalCaps = &event.RoomFeatures{
|
|||
"image/jpeg": event.CapLevelFullySupported,
|
||||
"image/webp": event.CapLevelFullySupported,
|
||||
"image/bmp": event.CapLevelFullySupported,
|
||||
"image/avif": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxWidth: 4096,
|
||||
MaxHeight: 4096,
|
||||
|
|
@ -99,8 +98,6 @@ var signalCaps = &event.RoomFeatures{
|
|||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"audio/aac": event.CapLevelFullySupported,
|
||||
"audio/mpeg": event.CapLevelFullySupported,
|
||||
"audio/mp3": event.CapLevelFullySupported,
|
||||
"audio/flac": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxSize: MaxFileSize,
|
||||
},
|
||||
|
|
@ -114,8 +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(),
|
||||
|
|
@ -142,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,
|
||||
|
|
@ -156,12 +152,7 @@ var signalCaps = &event.RoomFeatures{
|
|||
},
|
||||
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
|
||||
LocationMessage: event.CapLevelPartialSupport,
|
||||
Poll: event.CapLevelFullySupported,
|
||||
PollEnd: event.CapLevelUnsupported,
|
||||
PollHiddenVotes: event.CapLevelUnsupported,
|
||||
PollDuplicateOptions: event.CapLevelFullySupported,
|
||||
PollMaxOptions: 10,
|
||||
PollOptionMaxLength: 100,
|
||||
Poll: event.CapLevelRejected,
|
||||
Thread: event.CapLevelUnsupported,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
|
|
@ -178,12 +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{
|
||||
|
|
@ -198,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
|
||||
|
|
@ -220,7 +205,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
|||
AggressiveUpdateInfo: true,
|
||||
ImplicitReadReceipts: true,
|
||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||
ImagePackImport: true,
|
||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||
CreateDM: true,
|
||||
LookupPhone: true,
|
||||
|
|
@ -245,5 +229,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
|
|||
}
|
||||
|
||||
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
|
||||
return 1, 11
|
||||
return 1, 6
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
|
|||
e164String := fmt.Sprintf("+%d", e164Number)
|
||||
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
|
||||
return nil, fmt.Errorf("error looking up number in local contact list: %w", err)
|
||||
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
|
||||
} else if recipient != nil {
|
||||
aci = recipient.ACI
|
||||
pni = recipient.PNI
|
||||
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
|
||||
|
|
@ -228,9 +228,6 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
|
|||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
|
||||
}
|
||||
aci, pni = recipient.ACI, recipient.PNI
|
||||
if aci != uuid.Nil {
|
||||
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
aci, pni = serviceID.ToACIAndPNI()
|
||||
|
|
@ -326,13 +323,13 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
|
|||
}
|
||||
var avatarBytes []byte
|
||||
var avatarMXC id.ContentURIString
|
||||
if params.Avatar != nil && params.Avatar.URL != "" {
|
||||
if params.Avatar != nil {
|
||||
avatarMXC = params.Avatar.URL
|
||||
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil)
|
||||
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, params.Avatar.MSC3414File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
|
||||
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to upload avatar: %w", err)
|
||||
}
|
||||
|
|
@ -362,7 +359,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
|
|||
return nil, fmt.Errorf("failed to set portal room ID: %w", err)
|
||||
}
|
||||
}
|
||||
resp, err := s.Client.CreateGroup(ctx, group)
|
||||
resp, err := s.Client.CreateGroup(ctx, group, avatarBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create group: %w", err)
|
||||
}
|
||||
|
|
@ -414,7 +411,7 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
|
|||
}
|
||||
|
||||
func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
|
||||
namePtr := bridgev2.DefaultChatName
|
||||
name := ""
|
||||
topic := PrivateChatTopic
|
||||
selfUser := s.makeEventSender(s.Client.Store.ACI)
|
||||
members := &bridgev2.ChatMemberList{
|
||||
|
|
@ -441,7 +438,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
|
|||
var serviceID libsignalgo.ServiceID
|
||||
var avatar *bridgev2.Avatar
|
||||
if recipient.ACI == uuid.Nil {
|
||||
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient))
|
||||
name = s.Main.Config.FormatDisplayname(recipient)
|
||||
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
|
||||
} else {
|
||||
if backupChat == nil {
|
||||
|
|
@ -453,7 +450,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
|
|||
}
|
||||
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
|
||||
if recipient.ACI == s.Client.Store.ACI {
|
||||
namePtr = ptr.Ptr(NoteToSelfName)
|
||||
name = NoteToSelfName
|
||||
avatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
|
||||
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
|
||||
|
|
@ -474,14 +471,14 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
|
|||
return &bridgev2.CreateChatResponse{
|
||||
PortalKey: s.makeDMPortalKey(serviceID),
|
||||
PortalInfo: &bridgev2.ChatInfo{
|
||||
Name: namePtr,
|
||||
Name: &name,
|
||||
Avatar: avatar,
|
||||
Topic: &topic,
|
||||
Members: members,
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
|
||||
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
|
||||
CanBackfill: backupChat != nil,
|
||||
|
||||
ExtraUpdates: updatePortalSyncMeta,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,10 @@ import (
|
|||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
func (s *SignalClient) stopChatSync() {
|
||||
if cancel := s.cancelChatSync.Swap(nil); cancel != nil {
|
||||
(*cancel)()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) {
|
||||
defer cancel()
|
||||
|
||||
func (s *SignalClient) syncChats(ctx context.Context) {
|
||||
if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced {
|
||||
return
|
||||
}
|
||||
|
||||
if s.Client.Store.EphemeralBackupKey != nil {
|
||||
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
|
||||
meta, err := s.Client.WaitForTransfer(ctx)
|
||||
|
|
@ -74,22 +65,10 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
|
|||
}
|
||||
zerolog.Ctx(ctx).Info().Int("chat_count", len(chats)).Msg("Fetched chats to sync from database")
|
||||
for _, chat := range chats {
|
||||
if ctx.Err() != nil {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
AnErr("ctx_err", ctx.Err()).
|
||||
Msg("Context cancelled while syncing chats, stopping")
|
||||
return
|
||||
}
|
||||
recipient, err := s.Client.Store.BackupStore.GetBackupRecipient(ctx, chat.RecipientId)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat")
|
||||
continue
|
||||
} else if recipient == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Uint64("backup_chat_id", chat.Id).
|
||||
Uint64("backup_recipient_id", chat.RecipientId).
|
||||
Msg("No recipient found for chat")
|
||||
continue
|
||||
}
|
||||
resyncEvt := &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
|
|
@ -151,10 +130,7 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
|
|||
groupID := types.GroupIdentifier(base64.StdEncoding.EncodeToString(rawGroupID[:]))
|
||||
groupInfo, err := s.getGroupInfo(ctx, groupID, dest.Group.GetSnapshot().GetVersion(), chat)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Uint64("recipient_id", recipient.Id).
|
||||
Stringer("group_id", groupID).
|
||||
Msg("Failed to get full group info")
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full group info")
|
||||
continue
|
||||
}
|
||||
resyncEvt.PortalKey = s.makePortalKey(string(groupID))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package connector
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -27,7 +26,6 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
|
|
@ -41,13 +39,11 @@ type SignalClient struct {
|
|||
Ghost *bridgev2.Ghost
|
||||
|
||||
queueEmptyWaiter *exsync.Event
|
||||
cancelChatSync atomic.Pointer[context.CancelFunc]
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
|
||||
)
|
||||
|
||||
var pushCfg = &bridgev2.PushConfig{
|
||||
|
|
@ -78,27 +74,18 @@ func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType b
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
||||
return s.Main.MsgConv.DownloadImagePack(ctx, url)
|
||||
}
|
||||
|
||||
func (s *SignalClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
||||
return []*event.ImagePackMetadata{}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) LogoutRemote(ctx context.Context) {
|
||||
if s.Client == nil {
|
||||
return
|
||||
}
|
||||
s.stopChatSync()
|
||||
err := s.Client.Unlink(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
|
||||
}
|
||||
err = s.Client.StopReceiveLoops()
|
||||
err := s.Client.StopReceiveLoops()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout")
|
||||
}
|
||||
err = s.Client.Unlink(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
|
||||
}
|
||||
err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store")
|
||||
|
|
@ -189,7 +176,6 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
|
|||
}
|
||||
|
||||
case signalmeow.SignalConnectionEventLoggedOut:
|
||||
s.stopChatSync()
|
||||
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
|
||||
if err == nil {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
|
||||
|
|
@ -288,7 +274,6 @@ func (s *SignalClient) Disconnect() {
|
|||
if s.Client == nil {
|
||||
return
|
||||
}
|
||||
s.stopChatSync()
|
||||
err := s.Client.StopReceiveLoops()
|
||||
if err != nil {
|
||||
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
|
||||
|
|
@ -296,20 +281,28 @@ func (s *SignalClient) Disconnect() {
|
|||
}
|
||||
|
||||
func (s *SignalClient) postLoginConnect() {
|
||||
ctx := s.UserLogin.Log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
ctx := s.UserLogin.Log.WithContext(context.Background())
|
||||
// TODO it would be more proper to only connect after syncing,
|
||||
// but currently syncing will fetch group info online, so it has to be connected.
|
||||
s.tryConnect(ctx, 0, false)
|
||||
if s.Client.Store.EphemeralBackupKey != nil {
|
||||
go func() {
|
||||
s.syncChats(ctx)
|
||||
if s.Client.Store.MasterKey != nil {
|
||||
s.Client.SyncStorage(ctx)
|
||||
}
|
||||
}()
|
||||
} else if s.Client.Store.MasterKey != nil {
|
||||
go s.Client.SyncStorage(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSync bool) {
|
||||
if ctx.Err() != nil {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("retry_count", retryCount).
|
||||
AnErr("ctx_err", ctx.Err()).
|
||||
Msg("Context is canceled, not trying to connect")
|
||||
return
|
||||
}
|
||||
if retryCount == 0 {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
||||
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, doSync bool) {
|
||||
err := s.Client.RegisterCapabilities(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
|
||||
}
|
||||
ch, err := s.Client.StartReceiveLoops(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -320,39 +313,12 @@ func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSy
|
|||
retryInSeconds = 150
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
|
||||
select {
|
||||
case <-time.After(time.Duration(retryInSeconds) * time.Second):
|
||||
case <-ctx.Done():
|
||||
zerolog.Ctx(ctx).Info().Msg("Context canceled, exit tryConnect")
|
||||
return
|
||||
}
|
||||
s.tryConnect(ctx, retryCount+1, noLoginSync)
|
||||
return
|
||||
}
|
||||
syncCtx, cancel := context.WithCancel(ctx)
|
||||
if oldCancel := s.cancelChatSync.Swap(&cancel); oldCancel != nil {
|
||||
(*oldCancel)()
|
||||
}
|
||||
time.Sleep(time.Duration(retryInSeconds) * time.Second)
|
||||
s.tryConnect(ctx, retryCount+1, doSync)
|
||||
} else {
|
||||
go s.bridgeStateLoop(ch)
|
||||
if noLoginSync {
|
||||
go s.syncChats(syncCtx, cancel)
|
||||
} else {
|
||||
// TODO it would be more proper to only connect after syncing,
|
||||
// but currently syncing will fetch group info online, so it has to be connected.
|
||||
if s.Client.Store.EphemeralBackupKey != nil {
|
||||
go func() {
|
||||
if s.Client.Store.MasterKey != nil {
|
||||
s.Client.SyncStorage(ctx)
|
||||
} else {
|
||||
s.UserLogin.Log.Warn().Msg("No master key for storage sync before backup sync")
|
||||
}
|
||||
s.syncChats(syncCtx, cancel)
|
||||
}()
|
||||
} else {
|
||||
cancel()
|
||||
if s.Client.Store.MasterKey != nil {
|
||||
go s.Client.SyncStorage(ctx)
|
||||
}
|
||||
if doSync {
|
||||
go s.syncChats(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
|
@ -29,7 +29,6 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
|
|||
return nil, fmt.Errorf("failed to parse direct media id: %w", err)
|
||||
}
|
||||
|
||||
var rawDataResp []byte
|
||||
switch info := info.(type) {
|
||||
case *signalid.DirectMediaAttachment:
|
||||
log.Info().
|
||||
|
|
@ -42,15 +41,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
|
|||
Uint32("size", info.Size).
|
||||
Msg("Direct downloading attachment")
|
||||
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
|
||||
_, err := signalmeow.DownloadAttachment(
|
||||
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size, w,
|
||||
return &mediaproxy.GetMediaResponseCallback{
|
||||
Callback: func(w io.Writer) (int64, error) {
|
||||
data, err := signalmeow.DownloadAttachment(
|
||||
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return 0, err
|
||||
}
|
||||
return &mediaproxy.FileMeta{}, nil
|
||||
|
||||
_, err = w.Write(data)
|
||||
return int64(info.Size), err
|
||||
},
|
||||
}, nil
|
||||
case *signalid.DirectMediaGroupAvatar:
|
||||
|
|
@ -76,11 +78,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
|
|||
return nil, fmt.Errorf("failed to to get group master key: %w", err)
|
||||
}
|
||||
|
||||
rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
|
||||
return &mediaproxy.GetMediaResponseCallback{
|
||||
Callback: func(w io.Writer) (int64, error) {
|
||||
data, err := client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = w.Write(data)
|
||||
return int64(len(data)), err
|
||||
},
|
||||
}, nil
|
||||
case *signalid.DirectMediaProfileAvatar:
|
||||
log.Info().
|
||||
Stringer("user_id", info.UserID).
|
||||
|
|
@ -104,27 +113,19 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
|
|||
return nil, fmt.Errorf("profile key not found")
|
||||
}
|
||||
|
||||
rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
|
||||
return &mediaproxy.GetMediaResponseCallback{
|
||||
Callback: func(w io.Writer) (int64, error) {
|
||||
data, err := client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
case *signalid.DirectMediaSticker:
|
||||
log.Info().
|
||||
Hex("pack_id", info.PackID).
|
||||
Uint32("sticker_id", info.StickerID).
|
||||
Msg("Direct downloading sticker")
|
||||
|
||||
rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return int64(len(data)), err
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
|
||||
}
|
||||
if rawDataResp == nil {
|
||||
return nil, fmt.Errorf("unexpected fallthrough with no data")
|
||||
}
|
||||
return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
|
@ -53,9 +52,6 @@ var (
|
|||
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.DeleteChatHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.PollHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.MessageRequestAcceptingNetworkAPI = (*SignalClient)(nil)
|
||||
)
|
||||
|
||||
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
|
||||
|
|
@ -74,17 +70,17 @@ func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.Porta
|
|||
Int("failed_to_send_to_count", len(result.FailedToSendTo)).
|
||||
Int("successfully_sent_to_count", len(result.SuccessfullySentTo)).
|
||||
Logger()
|
||||
if len(result.FailedToSendTo) > 0 {
|
||||
log.Error().Msg("Failed to send event to some members of Signal group")
|
||||
}
|
||||
if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 {
|
||||
log.Debug().Msg("No successes or failures - Probably sent to myself")
|
||||
} else if len(result.SuccessfullySentTo) == 0 {
|
||||
log.Error().Msg("Failed to send event to all members of Signal group")
|
||||
return errors.New("failed to send to any members of Signal group")
|
||||
|
||||
} else if len(result.SuccessfullySentTo) < totalRecipients {
|
||||
if len(result.FailedToSendTo) > 0 {
|
||||
log.Warn().Msg("Failed to send event to some members of Signal group")
|
||||
} else {
|
||||
log.Warn().Msg("Only sent event to some members of Signal group")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("Sent event to all members of Signal group")
|
||||
}
|
||||
|
|
@ -113,31 +109,16 @@ func getTimestampForEvent(txnID networkid.RawTransactionID, evt *event.Event, or
|
|||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
converted, err := s.Main.MsgConv.ToSignal(
|
||||
ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo,
|
||||
ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, msg.ReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{
|
||||
ContainsAttachments: len(converted.Attachments) > 0,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SignalClient) doSendMessage(
|
||||
ctx context.Context,
|
||||
msg *bridgev2.MatrixMessage,
|
||||
converted *signalpb.DataMessage,
|
||||
meta *signalid.MessageMetadata,
|
||||
) (*bridgev2.MatrixMessageResponse, error) {
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
converted.Timestamp = &ts
|
||||
if meta == nil {
|
||||
meta = &signalid.MessageMetadata{}
|
||||
}
|
||||
msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts)
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(msgID))
|
||||
err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted))
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{DataMessage: converted})
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
|
||||
}
|
||||
|
|
@ -145,7 +126,9 @@ func (s *SignalClient) doSendMessage(
|
|||
ID: msgID,
|
||||
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||
Timestamp: time.UnixMilli(int64(ts)),
|
||||
Metadata: meta,
|
||||
Metadata: &signalid.MessageMetadata{
|
||||
ContainsAttachments: len(converted.Attachments) > 0,
|
||||
},
|
||||
}
|
||||
return &bridgev2.MatrixMessageResponse{
|
||||
DB: dbMsg,
|
||||
|
|
@ -167,56 +150,24 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
|
|||
return fmt.Errorf("failed to get message reply target: %w", err)
|
||||
}
|
||||
}
|
||||
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, replyTo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
converted.Timestamp = &ts
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapEditMessage(&signalpb.EditMessage{
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{EditMessage: &signalpb.EditMessage{
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
DataMessage: converted,
|
||||
}))
|
||||
}})
|
||||
if err != nil {
|
||||
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
|
||||
}
|
||||
prevID := msg.EditTarget.ID
|
||||
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts)
|
||||
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
|
||||
msg.EditTarget.EditCount++
|
||||
if prevID != msg.EditTarget.ID {
|
||||
err = s.Main.Bridge.DB.Message.Update(ctx, msg.EditTarget)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save message after editing")
|
||||
} else {
|
||||
saveEditStub(ctx, s.Main.Bridge, prevID, msg.EditTarget)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveEditStub saves a placeholder message row pointing at the pre-edit ID of a message, such that
|
||||
// duplicate checks on incoming edits find it and are dropped. This is necessary because the first
|
||||
// time we see an edit it modifies the ID in place.
|
||||
func saveEditStub(ctx context.Context, bridge *bridgev2.Bridge, prevID networkid.MessageID, target *database.Message) {
|
||||
stub := &database.Message{
|
||||
ID: prevID,
|
||||
PartID: editStubPartID,
|
||||
Room: target.Room,
|
||||
SenderID: target.SenderID,
|
||||
SenderMXID: target.SenderMXID,
|
||||
Timestamp: target.Timestamp,
|
||||
}
|
||||
stub.SetFakeMXID()
|
||||
err := bridge.DB.Message.Insert(ctx, stub)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Str("prev_message_id", string(prevID)).
|
||||
Str("message_id", string(target.ID)).
|
||||
Msg("Failed to save stub row for pre-edit message ID")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||
return bridgev2.MatrixReactionPreResponse{
|
||||
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||
|
|
@ -231,16 +182,19 @@ func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.M
|
|||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
wrappedContent := &signalpb.Content{
|
||||
DataMessage: &signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
|
||||
Reaction: &signalpb.DataMessage_Reaction{
|
||||
Emoji: proto.String(msg.PreHandleResp.Emoji),
|
||||
Remove: proto.Bool(false),
|
||||
TargetAuthorAciBinary: targetAuthorACI[:],
|
||||
TargetAuthorAci: proto.String(targetAuthorACI.String()),
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -253,16 +207,19 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
|
|||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
wrappedContent := &signalpb.Content{
|
||||
DataMessage: &signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
|
||||
Reaction: &signalpb.DataMessage_Reaction{
|
||||
Emoji: proto.String(msg.TargetReaction.Emoji),
|
||||
Remove: proto.Bool(true),
|
||||
TargetAuthorAciBinary: targetAuthorACI[:],
|
||||
TargetAuthorAci: proto.String(targetAuthorACI.String()),
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -277,12 +234,15 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg
|
|||
return fmt.Errorf("cannot delete other people's messages")
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
wrappedContent := &signalpb.Content{
|
||||
DataMessage: &signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
Delete: &signalpb.DataMessage_Delete{
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -342,21 +302,18 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
|
|||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error {
|
||||
userID, groupID, err := signalid.ParsePortalID(typing.Portal.ID)
|
||||
userID, _, err := signalid.ParsePortalID(typing.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
|
||||
// Only send typing notifications in DMs for now
|
||||
// Sending efficiently to groups requires implementing the proper SenderKey stuff first
|
||||
if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
|
||||
result := s.Client.SendMessage(ctx, userID, typingMessage)
|
||||
if !result.WasSuccessful {
|
||||
return result.Error
|
||||
}
|
||||
} else if groupID != "" {
|
||||
_, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -409,7 +366,7 @@ func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2
|
|||
return false, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
avatarHash = sha256.Sum256(data)
|
||||
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "")
|
||||
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to reupload avatar: %w", err)
|
||||
}
|
||||
|
|
@ -428,24 +385,22 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.
|
|||
}, nil)
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
||||
if msg.Type.IsSelf && msg.OrigSender != nil {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
|
||||
var targetIntent bridgev2.MatrixAPI
|
||||
var targetSignalID libsignalgo.ServiceID
|
||||
var err error
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
//TODO: this probably needs to revert some changes and clean up the portal on leaves
|
||||
switch msg.Type {
|
||||
case bridgev2.Invite:
|
||||
return nil, fmt.Errorf("cannot invite additional user to dm")
|
||||
return false, fmt.Errorf("cannot invite additional user to dm")
|
||||
default:
|
||||
return nil, nil
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target signal id: %w", err)
|
||||
return false, fmt.Errorf("failed to parse target signal id: %w", err)
|
||||
}
|
||||
switch target := msg.Target.(type) {
|
||||
case *bridgev2.Ghost:
|
||||
|
|
@ -455,12 +410,12 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
if targetIntent == nil {
|
||||
ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
|
||||
return false, fmt.Errorf("failed to get ghost for user: %w", err)
|
||||
}
|
||||
targetIntent = ghost.Intent
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
return false, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("From Membership", string(msg.Type.From)).
|
||||
|
|
@ -480,7 +435,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
switch msg.Type {
|
||||
case bridgev2.AcceptInvite:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't accept invite for non-ACI service ID")
|
||||
return false, fmt.Errorf("can't accept invite for non-ACI service ID")
|
||||
}
|
||||
gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{
|
||||
ACI: targetSignalID.UUID,
|
||||
|
|
@ -489,7 +444,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
case bridgev2.Leave, bridgev2.Kick:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't kick non-ACI service ID")
|
||||
return false, fmt.Errorf("can't kick non-ACI service ID")
|
||||
}
|
||||
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
case bridgev2.Invite:
|
||||
|
|
@ -525,7 +480,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
// }}
|
||||
case bridgev2.AcceptKnock:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't accept knock from non-ACI service ID")
|
||||
return false, fmt.Errorf("can't accept knock from non-ACI service ID")
|
||||
}
|
||||
gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{
|
||||
ACI: targetSignalID.UUID,
|
||||
|
|
@ -533,7 +488,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
}}
|
||||
case bridgev2.RetractKnock, bridgev2.RejectKnock:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't reject knock from non-ACI service ID")
|
||||
return false, fmt.Errorf("can't reject knock from non-ACI service ID")
|
||||
}
|
||||
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft:
|
||||
|
|
@ -544,39 +499,40 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
|
|||
switch msg.Type {
|
||||
case bridgev2.BanJoined:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
|
||||
return false, fmt.Errorf("can't ban joined non-ACI service ID")
|
||||
}
|
||||
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
case bridgev2.BanInvited:
|
||||
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
case bridgev2.BanKnocked:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
|
||||
return false, fmt.Errorf("can't ban knocked non-ACI service ID")
|
||||
}
|
||||
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
}
|
||||
case bridgev2.Unban:
|
||||
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To)
|
||||
log.Debug().Msg("unsupported membership change")
|
||||
return false, nil
|
||||
}
|
||||
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil || groupID == "" {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
|
||||
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
if (msg.Type == bridgev2.Invite || msg.Type == bridgev2.AcceptKnock) && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
|
||||
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
|
||||
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
|
||||
return nil, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func plToRole(pl int) signalmeow.GroupMemberRole {
|
||||
|
|
@ -710,11 +666,13 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
|
|||
})
|
||||
} else {
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
res := s.Client.SendMessage(ctx, userID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
|
||||
DataMessage: &signalpb.DataMessage{
|
||||
Timestamp: ptr.Ptr(ts),
|
||||
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
|
||||
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
|
||||
}))
|
||||
},
|
||||
})
|
||||
if !res.WasSuccessful {
|
||||
return false, res.Error
|
||||
}
|
||||
|
|
@ -722,231 +680,3 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
|
|||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
|
||||
userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
}
|
||||
|
||||
if msg.Content.FromMessageRequest {
|
||||
// TODO block and delete support?
|
||||
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send message request delete sync: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build ConversationIdentifier based on portal type
|
||||
var conversationID *signalpb.ConversationIdentifier
|
||||
if groupID == "" {
|
||||
conversationID = &signalpb.ConversationIdentifier{
|
||||
Identifier: &signalpb.ConversationIdentifier_ThreadServiceIdBinary{
|
||||
ThreadServiceIdBinary: userID.Bytes(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
gid, err := groupID.Bytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse group ID: %w", err)
|
||||
}
|
||||
conversationID = &signalpb.ConversationIdentifier{
|
||||
Identifier: &signalpb.ConversationIdentifier_ThreadGroupId{
|
||||
ThreadGroupId: gid[:],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve most recent messages from the portal
|
||||
var mostRecentMessages []*signalpb.AddressableMessage
|
||||
dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(
|
||||
ctx,
|
||||
msg.Portal.PortalKey,
|
||||
time.Now().Add(-30*24*time.Hour), // Last 30 days
|
||||
time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get recent messages for conversation delete")
|
||||
} else if len(dbMessages) > 0 {
|
||||
// Limit to the 5 most recent messages overall
|
||||
limit := 5
|
||||
startIdx := 0
|
||||
if len(dbMessages) > limit {
|
||||
startIdx = len(dbMessages) - limit
|
||||
}
|
||||
|
||||
// Create AddressableMessage for most recent messages
|
||||
for _, dbMsg := range dbMessages[startIdx:] {
|
||||
senderACI, timestamp, err := signalid.ParseMessageID(dbMsg.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
mostRecentMessages = append(mostRecentMessages, &signalpb.AddressableMessage{
|
||||
Author: &signalpb.AddressableMessage_AuthorServiceIdBinary{
|
||||
AuthorServiceIdBinary: senderACI[:],
|
||||
},
|
||||
SentTimestamp: proto.Uint64(timestamp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
recipientID := s.Client.Store.ACIServiceID()
|
||||
// Send DeleteForMe sync message to self
|
||||
result := s.Client.SendMessage(ctx, recipientID, signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
|
||||
Content: &signalpb.SyncMessage_DeleteForMe_{
|
||||
DeleteForMe: &signalpb.SyncMessage_DeleteForMe{
|
||||
ConversationDeletes: []*signalpb.SyncMessage_DeleteForMe_ConversationDelete{{
|
||||
Conversation: conversationID,
|
||||
MostRecentMessages: mostRecentMessages,
|
||||
IsFullDelete: proto.Bool(true),
|
||||
}},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("portal_id", string(msg.Portal.ID)).
|
||||
Int("recent_messages_count", len(mostRecentMessages)).
|
||||
Msg("Sent conversation deletion to Signal")
|
||||
|
||||
if !result.WasSuccessful {
|
||||
return fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
|
||||
optionNames := make([]string, len(msg.Content.PollStart.Answers))
|
||||
optionIDs := make([]string, len(msg.Content.PollStart.Answers))
|
||||
for i, option := range msg.Content.PollStart.Answers {
|
||||
optionNames[i] = option.Text
|
||||
optionIDs[i] = option.ID
|
||||
}
|
||||
converted := &signalpb.DataMessage{
|
||||
PollCreate: &signalpb.DataMessage_PollCreate{
|
||||
Question: ptr.Ptr(msg.Content.PollStart.Question.Text),
|
||||
AllowMultiple: ptr.Ptr(msg.Content.PollStart.MaxSelections != 1),
|
||||
Options: optionNames,
|
||||
},
|
||||
RequiredProtocolVersion: ptr.Ptr(uint32(signalpb.DataMessage_POLLS)),
|
||||
}
|
||||
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{
|
||||
MatrixPollOptionIDs: optionIDs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
|
||||
senderACI, msgTS, err := signalid.ParseMessageID(msg.VoteTo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta := msg.VoteTo.Metadata.(*signalid.MessageMetadata)
|
||||
mxOptions := meta.MatrixPollOptionIDs
|
||||
optionIndexes := make([]uint32, len(msg.Content.Response.Answers))
|
||||
for i, answer := range msg.Content.Response.Answers {
|
||||
if idx := slices.Index(mxOptions, answer); idx >= 0 {
|
||||
optionIndexes[i] = uint32(idx)
|
||||
} else if idx, err = strconv.Atoi(answer); err == nil && idx >= 0 {
|
||||
optionIndexes[i] = uint32(idx)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown poll answer ID: %s", answer)
|
||||
}
|
||||
}
|
||||
if meta.VoteCount == nil {
|
||||
meta.VoteCount = make(map[string]uint32)
|
||||
}
|
||||
meta.VoteCount[s.Client.Store.ACI.String()]++
|
||||
err = s.Main.Bridge.DB.Message.Update(ctx, msg.VoteTo)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to update poll message with new vote count")
|
||||
}
|
||||
converted := &signalpb.DataMessage{
|
||||
PollVote: &signalpb.DataMessage_PollVote{
|
||||
TargetAuthorAciBinary: senderACI[:],
|
||||
TargetSentTimestamp: &msgTS,
|
||||
OptionIndexes: optionIndexes,
|
||||
VoteCount: proto.Uint32(meta.VoteCount[s.Client.Store.ACI.String()]),
|
||||
},
|
||||
RequiredProtocolVersion: proto.Uint32(0),
|
||||
}
|
||||
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, nil)
|
||||
}
|
||||
|
||||
func (s *SignalClient) syncMessageRequestResponse(
|
||||
ctx context.Context,
|
||||
portal *bridgev2.Portal,
|
||||
respType signalpb.SyncMessage_MessageRequestResponse_Type,
|
||||
) error {
|
||||
userID, groupID, err := signalid.ParsePortalID(portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accept := &signalpb.SyncMessage_MessageRequestResponse{
|
||||
Type: respType.Enum(),
|
||||
}
|
||||
if groupID != "" {
|
||||
gidBytes, err := groupID.Bytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse group ID: %w", err)
|
||||
}
|
||||
accept.GroupId = gidBytes[:]
|
||||
} else if userID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
accept.ThreadAciBinary = userID.UUID[:]
|
||||
} else {
|
||||
return fmt.Errorf("invalid portal ID for message request response: %s", portal.ID)
|
||||
}
|
||||
res := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(s.Client.Store.ACI), signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
|
||||
Content: &signalpb.SyncMessage_MessageRequestResponse_{
|
||||
MessageRequestResponse: accept,
|
||||
},
|
||||
}))
|
||||
if !res.WasSuccessful {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg *bridgev2.MatrixAcceptMessageRequest) error {
|
||||
userID, _, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sync message request acceptance: %w", err)
|
||||
}
|
||||
if userID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get own profile key: %w", err)
|
||||
}
|
||||
var pniSig *signalpb.PniSignatureMessage
|
||||
if s.Client.Store.AccountRecord.GetPhoneNumberSharingMode() == signalpb.AccountRecord_EVERYBODY {
|
||||
sig, err := s.Client.Store.PNIIdentityKeyPair.SignAlternateIdentity(s.Client.Store.ACIIdentityKeyPair.GetIdentityKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate PNI signature: %w", err)
|
||||
}
|
||||
pniSig = &signalpb.PniSignatureMessage{
|
||||
Pni: s.Client.Store.PNI[:],
|
||||
Signature: sig,
|
||||
}
|
||||
}
|
||||
res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
|
||||
Content: &signalpb.Content_DataMessage{DataMessage: &signalpb.DataMessage{
|
||||
Flags: proto.Uint32(uint32(signalpb.DataMessage_PROFILE_KEY_UPDATE)),
|
||||
ProfileKey: profileKey.Slice(),
|
||||
Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)),
|
||||
|
||||
RequiredProtocolVersion: proto.Uint32(0),
|
||||
}},
|
||||
PniSignatureMessage: pniSig,
|
||||
})
|
||||
if !res.WasSuccessful {
|
||||
return fmt.Errorf("failed to share profile key to accept message request: %w", res.Error)
|
||||
}
|
||||
// TODO send read receipts too?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,12 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exzerolog"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
|
@ -38,7 +35,6 @@ import (
|
|||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
|
|
@ -56,8 +52,6 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
|
|||
return s.handleSignalReadSelf(evt)
|
||||
case *events.DeleteForMe:
|
||||
return s.handleSignalDeleteForMe(evt)
|
||||
case *events.MessageRequestResponse:
|
||||
return s.handleSignalMessageRequestResponse(evt)
|
||||
case *events.Call:
|
||||
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
|
||||
case *events.ContactList:
|
||||
|
|
@ -104,9 +98,6 @@ func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridg
|
|||
if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
|
||||
content.MsgType = event.MsgText
|
||||
}
|
||||
content.BeeperActionMessage = &event.BeeperActionMessage{
|
||||
Type: event.BeeperActionMessageCall,
|
||||
}
|
||||
} else {
|
||||
content.Body = "Call ended"
|
||||
}
|
||||
|
|
@ -176,7 +167,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
|
|||
case *signalpb.DataMessage:
|
||||
switch {
|
||||
case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil,
|
||||
innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil,
|
||||
innerEvt.Payment != nil, innerEvt.GiftBadge != nil,
|
||||
innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
|
||||
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
|
||||
return bridgev2.RemoteEventMessage
|
||||
|
|
@ -185,7 +176,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
|
|||
return bridgev2.RemoteEventReactionRemove
|
||||
}
|
||||
return bridgev2.RemoteEventReaction
|
||||
case innerEvt.Delete != nil, innerEvt.AdminDelete != nil:
|
||||
case innerEvt.Delete != nil:
|
||||
return bridgev2.RemoteEventMessageRemove
|
||||
case innerEvt.GetGroupV2().GetGroupChange() != nil:
|
||||
return bridgev2.RemoteEventChatInfoChange
|
||||
|
|
@ -294,21 +285,16 @@ func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
|
|||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
|
||||
var targetAuthorACI uuid.UUID
|
||||
var targetAuthorACI string
|
||||
var targetSentTS uint64
|
||||
switch innerEvt := evt.Event.(type) {
|
||||
case *signalpb.DataMessage:
|
||||
switch {
|
||||
case innerEvt.Reaction != nil:
|
||||
targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary())
|
||||
targetAuthorACI = innerEvt.Reaction.GetTargetAuthorAci()
|
||||
targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
|
||||
case innerEvt.Delete != nil:
|
||||
targetSentTS = innerEvt.Delete.GetTargetSentTimestamp()
|
||||
case innerEvt.AdminDelete != nil:
|
||||
if len(innerEvt.AdminDelete.GetTargetAuthorAciBinary()) == 16 {
|
||||
targetAuthorACI = uuid.UUID(innerEvt.AdminDelete.GetTargetAuthorAciBinary())
|
||||
}
|
||||
targetSentTS = innerEvt.AdminDelete.GetTargetSentTimestamp()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
|
@ -317,10 +303,11 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
|
|||
default:
|
||||
return ""
|
||||
}
|
||||
if targetAuthorACI == uuid.Nil {
|
||||
targetAuthorACI = evt.Info.Sender
|
||||
targetAuthorUUID := evt.Info.Sender
|
||||
if targetAuthorACI != "" {
|
||||
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
|
||||
}
|
||||
return signalid.MakeMessageID(targetAuthorACI, targetSentTS)
|
||||
return signalid.MakeMessageID(targetAuthorUUID, targetSentTS)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
|
||||
|
|
@ -340,7 +327,7 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
|
||||
}
|
||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, dataMsg, nil)
|
||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, dataMsg, nil)
|
||||
if converted.Disappear.Type != "" {
|
||||
evtTS := evt.GetTimestamp()
|
||||
if !dataMsg.GetIsViewOnce() {
|
||||
|
|
@ -359,52 +346,20 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
|
|||
return converted, nil
|
||||
}
|
||||
|
||||
const editStubPartID networkid.PartID = "editstub"
|
||||
|
||||
func isEditStub(msg *database.Message) bool {
|
||||
return msg.PartID == editStubPartID
|
||||
}
|
||||
|
||||
// editStubMessage returns a non-bridged message part which is saved as a placeholder row pointing
|
||||
// at the pre-edit ID of a message, such that duplicate checks on incoming edits find it and are
|
||||
// dropped. This is necessary because the first time we see an edit it modifies the ID in place.
|
||||
func editStubMessage() *bridgev2.ConvertedMessage {
|
||||
return &bridgev2.ConvertedMessage{
|
||||
Parts: []*bridgev2.ConvertedMessagePart{{
|
||||
ID: editStubPartID,
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{},
|
||||
DontBridge: true,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
|
||||
editMsg, ok := evt.Event.(*signalpb.EditMessage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
|
||||
}
|
||||
existing = slices.DeleteFunc(slices.Clone(existing), isEditStub)
|
||||
if len(existing) == 0 {
|
||||
return nil, fmt.Errorf("%w: edit target has already been edited", bridgev2.ErrIgnoringRemoteEvent)
|
||||
}
|
||||
// TODO tell converter about existing parts to avoid reupload?
|
||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, editMsg.GetDataMessage(), nil)
|
||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, editMsg.GetDataMessage(), nil)
|
||||
// TODO can anything other than the text be edited?
|
||||
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
|
||||
prevID := editPart.Part.ID
|
||||
// Clone the database message struct to avoid mutating the ID.
|
||||
// The ID from the original struct is used for AddedParts (we specifically want the old ID for that)
|
||||
editPart.Part = ptr.Clone(editPart.Part)
|
||||
editPart.Part.EditCount++
|
||||
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
|
||||
convertedEdit := &bridgev2.ConvertedEdit{
|
||||
return &bridgev2.ConvertedEdit{
|
||||
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
|
||||
}
|
||||
if prevID != editPart.Part.ID {
|
||||
convertedEdit.AddedParts = editStubMessage()
|
||||
}
|
||||
return convertedEdit, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetStreamOrder() int64 {
|
||||
|
|
@ -459,7 +414,7 @@ func (b *Bv2Receipt) GetReadUpTo() time.Time {
|
|||
return time.Time{}
|
||||
}
|
||||
|
||||
var _ bridgev2.RemoteReadReceipt = (*Bv2Receipt)(nil)
|
||||
var _ bridgev2.RemoteReceipt = (*Bv2Receipt)(nil)
|
||||
|
||||
func convertReceipts[T any](ctx context.Context, input []T, getMessageFunc func(ctx context.Context, msgID T) (*database.Message, error)) map[networkid.PortalKey]*Bv2Receipt {
|
||||
log := zerolog.Ctx(ctx)
|
||||
|
|
@ -505,7 +460,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
|
|||
Stringer("sender_id", evt.Sender).
|
||||
Stringer("receipt_type", evt.Content.GetType()).
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
ctx := log.WithContext(context.TODO())
|
||||
receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
|
||||
return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS))
|
||||
})
|
||||
|
|
@ -516,9 +471,9 @@ func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) bool {
|
|||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle signal read self").
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
ctx := log.WithContext(context.TODO())
|
||||
receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) {
|
||||
aciUUID, err := signalmeow.ParseStringOrBinaryUUID(msgInfo.GetSenderAci(), msgInfo.GetSenderAciBinary())
|
||||
aciUUID, err := uuid.Parse(msgInfo.GetSenderAci())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -537,13 +492,6 @@ func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signa
|
|||
return networkid.PortalKey{}, false
|
||||
}
|
||||
return s.makeDMPortalKey(serviceID), true
|
||||
case *signalpb.ConversationIdentifier_ThreadServiceIdBinary:
|
||||
serviceID, err := libsignalgo.ServiceIDFromBytes(ident.ThreadServiceIdBinary)
|
||||
if err != nil {
|
||||
log.Err(err).Hex("chat_id", ident.ThreadServiceIdBinary).Msg("Failed to parse delete for me conversation ID")
|
||||
return networkid.PortalKey{}, false
|
||||
}
|
||||
return s.makeDMPortalKey(serviceID), true
|
||||
case *signalpb.ConversationIdentifier_ThreadGroupId:
|
||||
if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
|
||||
log.Error().
|
||||
|
|
@ -582,22 +530,6 @@ func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey net
|
|||
return ""
|
||||
}
|
||||
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
|
||||
case *signalpb.AddressableMessage_AuthorServiceIdBinary:
|
||||
serviceID, err := libsignalgo.ServiceIDFromBytes(typedAuthor.AuthorServiceIdBinary)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portalKey).
|
||||
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
|
||||
Msg("Failed to parse delete for me message author service ID")
|
||||
return ""
|
||||
} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
log.Warn().
|
||||
Object("portal_key", portalKey).
|
||||
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
|
||||
Msg("Dropping delete for me message with unsupported service ID type")
|
||||
return ""
|
||||
}
|
||||
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
|
||||
case *signalpb.AddressableMessage_AuthorE164:
|
||||
log.Warn().
|
||||
Object("portal_key", portalKey).
|
||||
|
|
@ -688,45 +620,13 @@ func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalMessageRequestResponse(evt *events.MessageRequestResponse) bool {
|
||||
if evt.Type != signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
|
||||
// TODO do we need to do anything with blocks/deletes here or are they sent as normal delete events?
|
||||
return true
|
||||
}
|
||||
var portalKey networkid.PortalKey
|
||||
if evt.GroupID != nil {
|
||||
portalKey = s.makePortalKey(evt.GroupID.String())
|
||||
} else if evt.ThreadACI != uuid.Nil {
|
||||
portalKey = s.makeDMPortalKey(libsignalgo.NewACIServiceID(evt.ThreadACI))
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
PortalKey: portalKey,
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
StreamOrder: int64(evt.Timestamp),
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("action", "unmark message request").Str("source", "sync message")
|
||||
},
|
||||
},
|
||||
ChatInfoChange: &bridgev2.ChatInfoChange{
|
||||
ChatInfo: &bridgev2.ChatInfo{
|
||||
MessageRequest: ptr.Ptr(false),
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
|
||||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle aci found").
|
||||
Stringer("aci", evt.ACI).
|
||||
Stringer("pni", evt.PNI).
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
ctx := log.WithContext(context.TODO())
|
||||
pniPortalKey := s.makeDMPortalKey(evt.PNI)
|
||||
aciPortalKey := s.makeDMPortalKey(evt.ACI)
|
||||
result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey)
|
||||
|
|
@ -746,7 +646,7 @@ func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
|
|||
|
||||
func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
|
||||
log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
ctx := log.WithContext(context.TODO())
|
||||
for _, contact := range evt.Contacts {
|
||||
if contact.ACI == uuid.Nil {
|
||||
continue
|
||||
|
|
@ -774,33 +674,6 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
|
|||
if contact.ACI == s.Client.Store.ACI {
|
||||
s.updateRemoteProfile(ctx, true)
|
||||
}
|
||||
if ptr.Val(contact.Whitelisted) {
|
||||
portal, err := s.Main.Bridge.GetExistingPortalByKey(ctx, s.makeDMPortalKey(libsignalgo.NewACIServiceID(contact.ACI)))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get existing portal to update contact info")
|
||||
continue
|
||||
} else if portal != nil && portal.MessageRequest {
|
||||
s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("action", "unmark message request").Str("source", "contact list")
|
||||
},
|
||||
PortalKey: portal.PortalKey,
|
||||
},
|
||||
ChatInfoChange: &bridgev2.ChatInfoChange{
|
||||
ChatInfo: &bridgev2.ChatInfo{
|
||||
MessageRequest: ptr.Ptr(false),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).LastContactSync = jsontime.UnixMilliNow()
|
||||
err := s.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update last contact sync time")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,9 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/google/uuid"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
|
@ -44,7 +41,7 @@ func (s *SignalConnector) GetLoginFlows() []bridgev2.LoginFlow {
|
|||
|
||||
func (s *SignalConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
if flowID != "qr" {
|
||||
return nil, bridgev2.ErrInvalidLoginFlowID
|
||||
return nil, fmt.Errorf("invalid login flow ID")
|
||||
}
|
||||
return &QRLogin{User: user, Main: s}, nil
|
||||
}
|
||||
|
|
@ -55,6 +52,8 @@ type QRLogin struct {
|
|||
cancelChan context.CancelFunc
|
||||
ProvChan chan signalmeow.ProvisioningResponse
|
||||
newQRCount int
|
||||
|
||||
ProvData *store.DeviceData
|
||||
}
|
||||
|
||||
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
|
||||
|
|
@ -69,84 +68,16 @@ func (qr *QRLogin) Cancel() {
|
|||
|
||||
const (
|
||||
LoginStepQR = "fi.mau.signal.login.qr"
|
||||
LoginStepProcess = "fi.mau.signal.login.processing"
|
||||
LoginStepComplete = "fi.mau.signal.login.complete"
|
||||
)
|
||||
|
||||
const (
|
||||
qrRefreshInterval = 45 * time.Second
|
||||
maxQRRefreshes = 20
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLoginTimedOut = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.BRIDGE.LOGIN_TIMED_OUT",
|
||||
Err: "The QR code wasn't scanned in time, please start a new login",
|
||||
StatusCode: http.StatusGone,
|
||||
}
|
||||
ErrLoginCancelled = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.BRIDGE.LOGIN_CANCELLED",
|
||||
Err: "Login process was cancelled",
|
||||
StatusCode: http.StatusGone,
|
||||
}
|
||||
ErrDeviceLinkMissingCapability = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_MISSING_CAPABILITY",
|
||||
Err: "Signal rejected linking because the bridge is missing a capability required by your account's other devices. Please try again later",
|
||||
StatusCode: http.StatusConflict,
|
||||
}
|
||||
ErrDeviceLimitReached = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.SIGNAL.DEVICE_LIMIT_REACHED",
|
||||
Err: "Your Signal account already has the maximum number of linked devices. Remove one in the Signal app and try again",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
ErrDeviceLinkCodeInvalid = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_CODE_INVALID",
|
||||
Err: "The scanned QR code was invalid or already used, please start a new login",
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
ErrDeviceLinkRateLimited = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED",
|
||||
Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again",
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
}
|
||||
ErrDeviceLinkRejected = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED",
|
||||
Err: "Signal rejected linking the device",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
)
|
||||
|
||||
// Statuses of PUT /v1/devices/link, per Signal-Server's DeviceController
|
||||
func wrapProvisioningError(err error) error {
|
||||
var linkErr signalmeow.DeviceLinkError
|
||||
if errors.As(err, &linkErr) {
|
||||
switch linkErr.StatusCode {
|
||||
case http.StatusConflict:
|
||||
return ErrDeviceLinkMissingCapability
|
||||
case http.StatusLengthRequired:
|
||||
return ErrDeviceLimitReached
|
||||
case http.StatusForbidden:
|
||||
return ErrDeviceLinkCodeInvalid
|
||||
case http.StatusTooManyRequests:
|
||||
return ErrDeviceLinkRateLimited
|
||||
default:
|
||||
if linkErr.Message != "" {
|
||||
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d: %s)", linkErr.StatusCode, linkErr.Message)
|
||||
}
|
||||
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d)", linkErr.StatusCode)
|
||||
}
|
||||
}
|
||||
if websocket.CloseStatus(err) == websocket.StatusGoingAway {
|
||||
return ErrLoginTimedOut
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
log := qr.Main.Bridge.Log.With().
|
||||
Str("action", "login").
|
||||
Stringer("user_id", qr.User.MXID).
|
||||
Logger()
|
||||
provCtx, cancel := context.WithCancel(log.WithContext(qr.Main.Bridge.BackgroundCtx))
|
||||
provCtx, cancel := context.WithCancel(log.WithContext(context.Background()))
|
||||
qr.cancelChan = cancel
|
||||
// Don't use the start context here: the channel will outlive the start request.
|
||||
qr.ProvChan = signalmeow.PerformProvisioning(
|
||||
|
|
@ -156,16 +87,14 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
select {
|
||||
case resp = <-qr.ProvChan:
|
||||
if resp.Err != nil {
|
||||
return nil, wrapProvisioningError(resp.Err)
|
||||
return nil, resp.Err
|
||||
} else if resp.State != signalmeow.StateProvisioningURLReceived {
|
||||
return nil, fmt.Errorf("unexpected state %v", resp.State)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, ErrLoginTimedOut
|
||||
}
|
||||
return nil, ErrLoginCancelled
|
||||
return nil, ctx.Err()
|
||||
// TODO separate timeout here?
|
||||
}
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
|
|
@ -183,11 +112,19 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
return nil, fmt.Errorf("login not started")
|
||||
}
|
||||
|
||||
if qr.ProvData == nil {
|
||||
return qr.qrWait(ctx)
|
||||
} else {
|
||||
return qr.processingWait(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (qr *QRLogin) qrWait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
select {
|
||||
case resp := <-qr.ProvChan:
|
||||
if resp.Err != nil {
|
||||
qr.cancelChan()
|
||||
return nil, wrapProvisioningError(resp.Err)
|
||||
return nil, resp.Err
|
||||
} else if resp.State != signalmeow.StateProvisioningDataReceived {
|
||||
qr.cancelChan()
|
||||
return nil, fmt.Errorf("unexpected state %v", resp.State)
|
||||
|
|
@ -195,34 +132,52 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
qr.cancelChan()
|
||||
return nil, fmt.Errorf("no signal account ID received")
|
||||
}
|
||||
return qr.loginComplete(ctx, resp.ProvisioningData)
|
||||
qr.ProvData = resp.ProvisioningData
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepProcess,
|
||||
Instructions: fmt.Sprintf("Processing login as %s...", resp.ProvisioningData.Number),
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeNothing,
|
||||
},
|
||||
}, nil
|
||||
|
||||
// Server will timeout the request after 60 seconds, but Signal Desktop opens
|
||||
// a new socket and gets a new QR code after 45 seconds. We should do the same.
|
||||
case <-time.After(qrRefreshInterval):
|
||||
case <-time.After(45 * time.Second):
|
||||
qr.cancelChan()
|
||||
qr.newQRCount++
|
||||
if qr.newQRCount >= maxQRRefreshes {
|
||||
return nil, ErrLoginTimedOut
|
||||
if qr.newQRCount >= 6 {
|
||||
return nil, fmt.Errorf("too many QR code refreshes")
|
||||
}
|
||||
return qr.Start(ctx)
|
||||
|
||||
case <-ctx.Done():
|
||||
qr.cancelChan()
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, ErrLoginTimedOut
|
||||
}
|
||||
return nil, ErrLoginCancelled
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData) (*bridgev2.LoginStep, error) {
|
||||
func (qr *QRLogin) processingWait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
defer qr.cancelChan()
|
||||
newLoginID := signalid.MakeUserLoginID(qr.ProvData.ACI)
|
||||
|
||||
select {
|
||||
case resp := <-qr.ProvChan:
|
||||
if resp.Err != nil {
|
||||
return nil, resp.Err
|
||||
} else if resp.State != signalmeow.StateProvisioningPreKeysRegistered {
|
||||
return nil, fmt.Errorf("unexpected state %v", resp.State)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
ul, err := qr.User.NewLogin(ctx, &database.UserLogin{
|
||||
ID: signalid.MakeUserLoginID(provData.ACI),
|
||||
RemoteName: provData.Number,
|
||||
ID: newLoginID,
|
||||
RemoteName: qr.ProvData.Number,
|
||||
RemoteProfile: status.RemoteProfile{
|
||||
Phone: provData.Number,
|
||||
Phone: qr.ProvData.Number,
|
||||
},
|
||||
Metadata: &signalid.UserLoginMetadata{},
|
||||
}, &bridgev2.NewLoginParams{
|
||||
|
|
@ -235,7 +190,7 @@ func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData
|
|||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
StepID: LoginStepComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", provData.Number, provData.ACI),
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", qr.ProvData.Number, qr.ProvData.ACI),
|
||||
CompleteParams: &bridgev2.LoginCompleteParams{
|
||||
UserLoginID: ul.ID,
|
||||
UserLogin: ul,
|
||||
|
|
|
|||
|
|
@ -20,18 +20,18 @@ package libsignalgo
|
|||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import "runtime"
|
||||
import (
|
||||
"runtime"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type AccountEntropyPool string
|
||||
type SVRKey = fixedArray32
|
||||
|
||||
func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
|
||||
var out SVRKey
|
||||
aepC, free := GoStringToCString(string(aep))
|
||||
defer free()
|
||||
var out [C.SignalSVR_KEY_LEN]byte
|
||||
signalFfiError := C.signal_account_entropy_pool_derive_svr_key(
|
||||
out.cFixedArray(),
|
||||
aepC,
|
||||
(*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
C.CString(string(aep)),
|
||||
)
|
||||
runtime.KeepAlive(aep)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -41,12 +41,10 @@ func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) {
|
||||
var out BackupKey
|
||||
aepC, free := GoStringToCString(string(aep))
|
||||
defer free()
|
||||
var out [C.SignalBACKUP_KEY_LEN]byte
|
||||
signalFfiError := C.signal_account_entropy_pool_derive_backup_key(
|
||||
out.cFixedArray(),
|
||||
aepC,
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
C.CString(string(aep)),
|
||||
)
|
||||
runtime.KeepAlive(aep)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ func NewUUIDAddressFromString(uuidStr string, deviceID uint) (*Address, error) {
|
|||
|
||||
func newAddress(name string, deviceID uint) (*Address, error) {
|
||||
var pa C.SignalMutPointerProtocolAddress
|
||||
nameStr, freeNameStr := GoStringToCString(name)
|
||||
defer freeNameStr()
|
||||
signalFfiError := C.signal_address_new(&pa, nameStr, C.uint(deviceID))
|
||||
signalFfiError := C.signal_address_new(&pa, C.CString(name), C.uint(deviceID))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
|
@ -83,7 +81,7 @@ func (pa *Address) CancelFinalizer() {
|
|||
}
|
||||
|
||||
func (pa *Address) Name() (string, error) {
|
||||
var name C.SignalCStringPtr
|
||||
var name *C.char
|
||||
signalFfiError := C.signal_address_get_name(&name, pa.constPtr())
|
||||
runtime.KeepAlive(pa)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -24,16 +24,15 @@ package libsignalgo
|
|||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// type AuthCredential [181]byte
|
||||
// type AuthCredentialResponse [361]byte
|
||||
const AuthCredentialWithPniLength = 265
|
||||
|
||||
type AuthCredentialWithPni [AuthCredentialWithPniLength]byte
|
||||
type AuthCredentialWithPniResponse [425]byte
|
||||
// type AuthCredential [C.SignalAUTH_CREDENTIAL_LEN]byte
|
||||
// type AuthCredentialResponse [C.SignalAUTH_CREDENTIAL_RESPONSE_LEN]byte
|
||||
type AuthCredentialWithPni [C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN]byte
|
||||
type AuthCredentialWithPniResponse [C.SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]byte
|
||||
type AuthCredentialPresentation []byte
|
||||
|
||||
func (ac *AuthCredentialWithPni) Slice() []byte {
|
||||
|
|
@ -52,8 +51,8 @@ func ReceiveAuthCredentialWithPni(
|
|||
signalFfiError := C.signal_server_public_params_receive_auth_credential_with_pni_as_service_id(
|
||||
&c_result,
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
NewACIServiceID(aci).cConstFixedArray(),
|
||||
NewPNIServiceID(pni).cConstFixedArray(),
|
||||
NewACIServiceID(aci).CFixedBytes(),
|
||||
NewPNIServiceID(pni).CFixedBytes(),
|
||||
C.uint64_t(redemptionTime),
|
||||
BytesToBuffer(authCredResponse[:]),
|
||||
)
|
||||
|
|
@ -61,8 +60,8 @@ func ReceiveAuthCredentialWithPni(
|
|||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
resultBytes := CopySignalOwnedBufferToBytes(c_result)
|
||||
if len(resultBytes) != AuthCredentialWithPniLength {
|
||||
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), AuthCredentialWithPniLength)
|
||||
if len(resultBytes) != C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN {
|
||||
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN)
|
||||
}
|
||||
return (*AuthCredentialWithPni)(resultBytes), nil
|
||||
}
|
||||
|
|
@ -84,12 +83,14 @@ func CreateAuthCredentialWithPniPresentation(
|
|||
authCredWithPni AuthCredentialWithPni,
|
||||
) (*AuthCredentialPresentation, error) {
|
||||
var c_result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
c_randomness := (*[C.SignalRANDOMNESS_LEN]C.uchar)(unsafe.Pointer(&randomness[0]))
|
||||
c_groupSecretParams := (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(&groupSecretParams[0]))
|
||||
|
||||
signalFfiError := C.signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic(
|
||||
&c_result,
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
randomness.cConstFixedArray(),
|
||||
groupSecretParams.cConstFixedArray(),
|
||||
c_randomness,
|
||||
c_groupSecretParams,
|
||||
BytesToBuffer(authCredWithPni[:]),
|
||||
)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ import (
|
|||
"go.mau.fi/util/random"
|
||||
)
|
||||
|
||||
const BackupKeyLength = 32
|
||||
|
||||
type BackupKey [BackupKeyLength]byte
|
||||
type BackupKey [C.SignalBACKUP_KEY_LEN]byte
|
||||
|
||||
func (bk *BackupKey) Slice() []byte {
|
||||
if bk == nil {
|
||||
|
|
@ -40,25 +38,17 @@ func (bk *BackupKey) Slice() []byte {
|
|||
|
||||
const BackupIDLength = 16
|
||||
|
||||
type BackupID = fixedArray16
|
||||
type BackupMetadataKey = fixedArray32
|
||||
type BackupMediaID = fixedArray15
|
||||
type BackupMediaKey = fixedArray64
|
||||
|
||||
func (bk *BackupKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
|
||||
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk))
|
||||
}
|
||||
|
||||
func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat {
|
||||
return cFixedArray32Compat(bk.cFixedArray())
|
||||
}
|
||||
type BackupID [BackupIDLength]byte
|
||||
type BackupMetadataKey [C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]byte
|
||||
type BackupMediaID [C.SignalMEDIA_ID_LEN]byte
|
||||
type BackupMediaKey [C.SignalMEDIA_ENCRYPTION_KEY_LEN]byte
|
||||
|
||||
func GenerateRandomBackupKey() *BackupKey {
|
||||
return (*BackupKey)(random.Bytes(BackupKeyLength))
|
||||
return (*BackupKey)(random.Bytes(C.SignalBACKUP_KEY_LEN))
|
||||
}
|
||||
|
||||
func BytesToBackupKey(bytes []byte) *BackupKey {
|
||||
if len(bytes) != BackupKeyLength {
|
||||
if len(bytes) != C.SignalBACKUP_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
return (*BackupKey)(bytes)
|
||||
|
|
@ -67,9 +57,9 @@ func BytesToBackupKey(bytes []byte) *BackupKey {
|
|||
func (bk *BackupKey) DeriveBackupID(aci ServiceID) (*BackupID, error) {
|
||||
var out BackupID
|
||||
signalFfiError := C.signal_backup_key_derive_backup_id(
|
||||
out.cFixedArray(),
|
||||
bk.cConstFixedArray(),
|
||||
aci.cConstFixedArray(),
|
||||
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
aci.CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -82,8 +72,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
|
|||
var out C.SignalMutPointerPrivateKey
|
||||
signalFfiError := C.signal_backup_key_derive_ec_key(
|
||||
&out,
|
||||
bk.cConstFixedArray(),
|
||||
aci.cConstFixedArray(),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&bk)),
|
||||
aci.CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -95,8 +85,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
|
|||
func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error) {
|
||||
var out BackupMetadataKey
|
||||
signalFfiError := C.signal_backup_key_derive_local_backup_metadata_key(
|
||||
out.cFixedArray(),
|
||||
bk.cConstFixedArray(),
|
||||
(*[C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -107,12 +97,10 @@ func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error)
|
|||
|
||||
func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
|
||||
var out BackupMediaID
|
||||
mediaNameStr, mediaNameFree := GoStringToCString(mediaName)
|
||||
defer mediaNameFree()
|
||||
signalFfiError := C.signal_backup_key_derive_media_id(
|
||||
out.cFixedArray(),
|
||||
bk.cConstFixedArray(),
|
||||
mediaNameStr,
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
C.CString(mediaName),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -124,9 +112,9 @@ func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
|
|||
func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
|
||||
var out BackupMediaKey
|
||||
signalFfiError := C.signal_backup_key_derive_media_encryption_key(
|
||||
out.cFixedArray(),
|
||||
bk.cConstFixedArray(),
|
||||
mediaID.cConstFixedArray(),
|
||||
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
runtime.KeepAlive(mediaID)
|
||||
|
|
@ -139,9 +127,9 @@ func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMe
|
|||
func (bk *BackupKey) DeriveThumbnailTransitEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
|
||||
var out BackupMediaKey
|
||||
signalFfiError := C.signal_backup_key_derive_thumbnail_transit_encryption_key(
|
||||
out.cFixedArray(),
|
||||
bk.cConstFixedArray(),
|
||||
mediaID.cConstFixedArray(),
|
||||
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
runtime.KeepAlive(mediaID)
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz -lstdc++
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz
|
||||
*/
|
||||
import "C"
|
||||
|
|
|
|||
|
|
@ -18,20 +18,12 @@ package libsignalgo
|
|||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
func GoStringToCString(str string) (C.SignalCStringPtr, func()) {
|
||||
cStr := C.CString(str)
|
||||
return C.SignalCStringPtr(unsafe.Pointer(cStr)), func() {
|
||||
C.free(unsafe.Pointer(cStr))
|
||||
}
|
||||
}
|
||||
|
||||
func CopyCStringToString(cString C.SignalCStringPtr) (s string) {
|
||||
s = C.GoString((*C.char)(unsafe.Pointer(cString)))
|
||||
func CopyCStringToString(cString *C.char) (s string) {
|
||||
s = C.GoString(cString)
|
||||
C.signal_free_string(cString)
|
||||
return
|
||||
}
|
||||
|
|
@ -47,17 +39,3 @@ func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
|
|||
C.signal_free_buffer(buffer.base, buffer.length)
|
||||
return
|
||||
}
|
||||
|
||||
func CopySignalBytestringArray[T ~[]byte](buffer C.SignalBytestringArray) (b []T) {
|
||||
concatted := C.GoBytes(unsafe.Pointer(buffer.bytes.base), C.int(buffer.bytes.length))
|
||||
b = make([]T, int(buffer.lengths.length))
|
||||
sizeTSize := unsafe.Sizeof(C.size_t(0))
|
||||
offset := 0
|
||||
for i := 0; i < int(buffer.lengths.length); i++ {
|
||||
length := int(*(*C.size_t)(unsafe.Add(unsafe.Pointer(buffer.lengths.base), uintptr(i)*sizeTSize)))
|
||||
b[i] = concatted[offset : offset+length]
|
||||
offset += length
|
||||
}
|
||||
C.signal_free_bytestring_array(buffer)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -43,9 +43,7 @@ func (dtk *DeviceTransferKey) PrivateKeyMaterial() []byte {
|
|||
|
||||
func (dtk *DeviceTransferKey) GenerateCertificate(name string, days int) ([]byte, error) {
|
||||
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
nameStr, freeNameStr := GoStringToCString(name)
|
||||
defer freeNameStr()
|
||||
signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), nameStr, C.uint32_t(days))
|
||||
signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), C.CString(name), C.uint32_t(days))
|
||||
runtime.KeepAlive(dtk)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -26,10 +26,6 @@ import (
|
|||
|
||||
type ErrorCode int
|
||||
|
||||
func (e ErrorCode) Error() string {
|
||||
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
|
||||
}
|
||||
|
||||
const (
|
||||
ErrorCodeUnknownError ErrorCode = 1
|
||||
ErrorCodeInvalidState ErrorCode = 2
|
||||
|
|
@ -38,7 +34,6 @@ const (
|
|||
ErrorCodeInvalidArgument ErrorCode = 5
|
||||
ErrorCodeInvalidType ErrorCode = 6
|
||||
ErrorCodeInvalidUtf8String ErrorCode = 7
|
||||
ErrorCodeCancelled ErrorCode = 8
|
||||
ErrorCodeProtobufError ErrorCode = 10
|
||||
ErrorCodeLegacyCiphertextVersion ErrorCode = 21
|
||||
ErrorCodeUnknownCiphertextVersion ErrorCode = 22
|
||||
|
|
@ -56,61 +51,9 @@ const (
|
|||
ErrorCodeInvalidRegistrationId ErrorCode = 81
|
||||
ErrorCodeInvalidSession ErrorCode = 82
|
||||
ErrorCodeInvalidSenderKeySession ErrorCode = 83
|
||||
ErrorCodeInvalidProtocolAddress ErrorCode = 84
|
||||
ErrorCodeDuplicatedMessage ErrorCode = 90
|
||||
ErrorCodeCallbackError ErrorCode = 100
|
||||
ErrorCodeVerificationFailure ErrorCode = 110
|
||||
ErrorCodeUsernameCannotBeEmpty ErrorCode = 120
|
||||
ErrorCodeUsernameCannotStartWithDigit ErrorCode = 121
|
||||
ErrorCodeUsernameMissingSeparator ErrorCode = 122
|
||||
ErrorCodeUsernameBadDiscriminatorCharacter ErrorCode = 123
|
||||
ErrorCodeUsernameBadNicknameCharacter ErrorCode = 124
|
||||
ErrorCodeUsernameTooShort ErrorCode = 125
|
||||
ErrorCodeUsernameTooLong ErrorCode = 126
|
||||
ErrorCodeUsernameLinkInvalidEntropyDataLength ErrorCode = 127
|
||||
ErrorCodeUsernameLinkInvalid ErrorCode = 128
|
||||
ErrorCodeUsernameDiscriminatorCannotBeEmpty ErrorCode = 130
|
||||
ErrorCodeUsernameDiscriminatorCannotBeZero ErrorCode = 131
|
||||
ErrorCodeUsernameDiscriminatorCannotBeSingleDigit ErrorCode = 132
|
||||
ErrorCodeUsernameDiscriminatorCannotHaveLeadingZeros ErrorCode = 133
|
||||
ErrorCodeUsernameDiscriminatorTooLarge ErrorCode = 134
|
||||
ErrorCodeIoError ErrorCode = 140
|
||||
ErrorCodeInvalidMediaInput ErrorCode = 141
|
||||
ErrorCodeUnsupportedMediaInput ErrorCode = 142
|
||||
ErrorCodeConnectionTimedOut ErrorCode = 143
|
||||
ErrorCodeNetworkProtocol ErrorCode = 144
|
||||
ErrorCodeRateLimited ErrorCode = 145
|
||||
ErrorCodeWebSocket ErrorCode = 146
|
||||
ErrorCodeCdsiInvalidToken ErrorCode = 147
|
||||
ErrorCodeConnectionFailed ErrorCode = 148
|
||||
ErrorCodeChatServiceInactive ErrorCode = 149
|
||||
ErrorCodeRequestTimedOut ErrorCode = 150
|
||||
ErrorCodeRateLimitChallenge ErrorCode = 151
|
||||
ErrorCodePossibleCaptiveNetwork ErrorCode = 152
|
||||
ErrorCodeSvrDataMissing ErrorCode = 160
|
||||
ErrorCodeSvrRestoreFailed ErrorCode = 161
|
||||
ErrorCodeSvrRotationMachineTooManySteps ErrorCode = 162
|
||||
ErrorCodeSvrRequestFailed ErrorCode = 163
|
||||
ErrorCodeAppExpired ErrorCode = 170
|
||||
ErrorCodeDeviceDeregistered ErrorCode = 171
|
||||
ErrorCodeConnectionInvalidated ErrorCode = 172
|
||||
ErrorCodeConnectedElsewhere ErrorCode = 173
|
||||
ErrorCodeBackupValidation ErrorCode = 180
|
||||
ErrorCodeRegistrationInvalidSessionId ErrorCode = 190
|
||||
ErrorCodeRegistrationUnknown ErrorCode = 192
|
||||
ErrorCodeRegistrationSessionNotFound ErrorCode = 193
|
||||
ErrorCodeRegistrationNotReadyForVerification ErrorCode = 194
|
||||
ErrorCodeRegistrationSendVerificationCodeFailed ErrorCode = 195
|
||||
ErrorCodeRegistrationCodeNotDeliverable ErrorCode = 196
|
||||
ErrorCodeRegistrationSessionUpdateRejected ErrorCode = 197
|
||||
ErrorCodeRegistrationCredentialsCouldNotBeParsed ErrorCode = 198
|
||||
ErrorCodeRegistrationDeviceTransferPossible ErrorCode = 199
|
||||
ErrorCodeRegistrationRecoveryVerificationFailed ErrorCode = 200
|
||||
ErrorCodeRegistrationLock ErrorCode = 201
|
||||
ErrorCodeKeyTransparencyError ErrorCode = 210
|
||||
ErrorCodeKeyTransparencyVerificationFailed ErrorCode = 211
|
||||
ErrorCodeRequestUnauthorized ErrorCode = 220
|
||||
ErrorCodeMismatchedDevices ErrorCode = 221
|
||||
)
|
||||
|
||||
type SignalError struct {
|
||||
|
|
@ -122,10 +65,6 @@ func (e *SignalError) Error() string {
|
|||
return fmt.Sprintf("%d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *SignalError) Unwrap() error {
|
||||
return e.Code
|
||||
}
|
||||
|
||||
func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error {
|
||||
if signalError == nil {
|
||||
return nil
|
||||
|
|
@ -152,7 +91,7 @@ func wrapError(signalError *C.SignalFfiError) error {
|
|||
}
|
||||
|
||||
func wrapSignalError(signalError *C.SignalFfiError, errorType C.uint32_t) error {
|
||||
var messageBytes C.SignalCStringPtr
|
||||
var messageBytes *C.char
|
||||
getMessageError := C.signal_error_get_message(&messageBytes, signalError)
|
||||
if getMessageError != nil {
|
||||
// Ignore any errors from this, it will just end up being an empty string.
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func (f *Fingerprint) ScannableEncoding() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (f *Fingerprint) DisplayString() (string, error) {
|
||||
var displayString C.SignalCStringPtr
|
||||
var displayString *C.char
|
||||
signalFfiError := C.signal_fingerprint_display_string(&displayString, f.constPtr())
|
||||
runtime.KeepAlive(f)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -1,159 +0,0 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2026 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
type fixedArray15 [15]byte
|
||||
type fixedArray16 [16]byte
|
||||
type fixedArray17 [17]byte
|
||||
type fixedArray32 [32]byte
|
||||
type fixedArray64 [64]byte
|
||||
type fixedArray65 [65]byte
|
||||
type fixedArray97 [97]byte
|
||||
type fixedArray129 [129]byte
|
||||
type fixedArray153 [153]byte
|
||||
type fixedArray177 [177]byte
|
||||
type fixedArray289 [289]byte
|
||||
type fixedArray329 [329]byte
|
||||
type fixedArray409 [409]byte
|
||||
type fixedArray473 [473]byte
|
||||
type fixedArray497 [497]byte
|
||||
|
||||
func (a *fixedArray15) cFixedArray() *C.SignalType_FixedArray15_uint8_t {
|
||||
return (*C.SignalType_FixedArray15_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray15) cConstFixedArray() cFixedArray15Compat {
|
||||
return cFixedArray15Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray16) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
|
||||
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray16) cConstFixedArray() cFixedArray16Compat {
|
||||
return cFixedArray16Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray17) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
|
||||
return (*C.SignalType_FixedArray17_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray17) cConstFixedArray() cFixedArray17Compat {
|
||||
return cFixedArray17Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray32) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
|
||||
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray32) cConstFixedArray() cFixedArray32Compat {
|
||||
return cFixedArray32Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray64) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
|
||||
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray64) cConstFixedArray() cFixedArray64Compat {
|
||||
return cFixedArray64Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray65) cFixedArray() *C.SignalType_FixedArray65_uint8_t {
|
||||
return (*C.SignalType_FixedArray65_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray65) cConstFixedArray() cFixedArray65Compat {
|
||||
return cFixedArray65Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray97) cFixedArray() *C.SignalType_FixedArray97_uint8_t {
|
||||
return (*C.SignalType_FixedArray97_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray97) cConstFixedArray() cFixedArray97Compat {
|
||||
return cFixedArray97Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray129) cFixedArray() *C.SignalType_FixedArray129_uint8_t {
|
||||
return (*C.SignalType_FixedArray129_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray129) cConstFixedArray() cFixedArray129Compat {
|
||||
return cFixedArray129Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray153) cFixedArray() *C.SignalType_FixedArray153_uint8_t {
|
||||
return (*C.SignalType_FixedArray153_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray153) cConstFixedArray() cFixedArray153Compat {
|
||||
return cFixedArray153Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray177) cFixedArray() *C.SignalType_FixedArray177_uint8_t {
|
||||
return (*C.SignalType_FixedArray177_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray177) cConstFixedArray() cFixedArray177Compat {
|
||||
return cFixedArray177Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray289) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
|
||||
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray289) cConstFixedArray() cFixedArray289Compat {
|
||||
return cFixedArray289Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray329) cFixedArray() *C.SignalType_FixedArray329_uint8_t {
|
||||
return (*C.SignalType_FixedArray329_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray329) cConstFixedArray() cFixedArray329Compat {
|
||||
return cFixedArray329Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray409) cFixedArray() *C.SignalType_FixedArray409_uint8_t {
|
||||
return (*C.SignalType_FixedArray409_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray409) cConstFixedArray() cFixedArray409Compat {
|
||||
return cFixedArray409Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray473) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
|
||||
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray473) cConstFixedArray() cFixedArray473Compat {
|
||||
return cFixedArray473Compat(a.cFixedArray())
|
||||
}
|
||||
|
||||
func (a *fixedArray497) cFixedArray() *C.SignalType_FixedArray497_uint8_t {
|
||||
return (*C.SignalType_FixedArray497_uint8_t)(unsafe.Pointer(a))
|
||||
}
|
||||
|
||||
func (a *fixedArray497) cConstFixedArray() cFixedArray497Compat {
|
||||
return cFixedArray497Compat(a.cFixedArray())
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
//go:build darwin || android || ios || (windows && arm64)
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type cFixedArray15Compat = *C.SignalType_FixedArray15_uint8_t
|
||||
type cFixedArray16Compat = *C.SignalType_FixedArray16_uint8_t
|
||||
type cFixedArray17Compat = *C.SignalType_FixedArray17_uint8_t
|
||||
type cFixedArray32Compat = *C.SignalType_FixedArray32_uint8_t
|
||||
type cFixedArray64Compat = *C.SignalType_FixedArray64_uint8_t
|
||||
type cFixedArray65Compat = *C.SignalType_FixedArray65_uint8_t
|
||||
type cFixedArray97Compat = *C.SignalType_FixedArray97_uint8_t
|
||||
type cFixedArray129Compat = *C.SignalType_FixedArray129_uint8_t
|
||||
type cFixedArray153Compat = *C.SignalType_FixedArray153_uint8_t
|
||||
type cFixedArray177Compat = *C.SignalType_FixedArray177_uint8_t
|
||||
type cFixedArray289Compat = *C.SignalType_FixedArray289_uint8_t
|
||||
type cFixedArray329Compat = *C.SignalType_FixedArray329_uint8_t
|
||||
type cFixedArray409Compat = *C.SignalType_FixedArray409_uint8_t
|
||||
type cFixedArray473Compat = *C.SignalType_FixedArray473_uint8_t
|
||||
type cFixedArray497Compat = *C.SignalType_FixedArray497_uint8_t
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//go:build !(darwin || android || ios || (windows && arm64))
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// Hack for https://github.com/golang/go/issues/7270
|
||||
// The clang version is more correct, but doesn't work with gcc.
|
||||
type cFixedArray15Compat = *[15]C.uint8_t
|
||||
type cFixedArray16Compat = *[16]C.uint8_t
|
||||
type cFixedArray17Compat = *[17]C.uint8_t
|
||||
type cFixedArray32Compat = *[32]C.uint8_t
|
||||
type cFixedArray64Compat = *[64]C.uint8_t
|
||||
type cFixedArray65Compat = *[65]C.uint8_t
|
||||
type cFixedArray97Compat = *[97]C.uint8_t
|
||||
type cFixedArray129Compat = *[129]C.uint8_t
|
||||
type cFixedArray153Compat = *[153]C.uint8_t
|
||||
type cFixedArray177Compat = *[177]C.uint8_t
|
||||
type cFixedArray289Compat = *[289]C.uint8_t
|
||||
type cFixedArray329Compat = *[329]C.uint8_t
|
||||
type cFixedArray409Compat = *[409]C.uint8_t
|
||||
type cFixedArray473Compat = *[473]C.uint8_t
|
||||
type cFixedArray497Compat = *[497]C.uint8_t
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ import (
|
|||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const RandomnessLength = 32
|
||||
|
||||
type Randomness = fixedArray32
|
||||
type Randomness [C.SignalRANDOMNESS_LEN]byte
|
||||
|
||||
func GenerateRandomness() Randomness {
|
||||
var randomness Randomness
|
||||
|
|
@ -44,39 +42,14 @@ func GenerateRandomness() Randomness {
|
|||
return randomness
|
||||
}
|
||||
|
||||
const GroupMasterKeyLength = 32
|
||||
const GroupIdentifierLength = 32
|
||||
const GroupSecretParamsLength = 289
|
||||
const GroupMasterKeyLength = C.SignalGROUP_MASTER_KEY_LEN
|
||||
const GroupIdentifierLength = C.SignalGROUP_IDENTIFIER_LEN
|
||||
|
||||
type GroupMasterKey [GroupMasterKeyLength]byte
|
||||
type GroupSecretParams [GroupSecretParamsLength]byte
|
||||
type GroupPublicParams = fixedArray97
|
||||
type GroupSecretParams [C.SignalGROUP_SECRET_PARAMS_LEN]byte
|
||||
type GroupPublicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]byte
|
||||
type GroupIdentifier [GroupIdentifierLength]byte
|
||||
|
||||
func (gmk *GroupMasterKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
|
||||
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gmk))
|
||||
}
|
||||
|
||||
func (gmk *GroupMasterKey) cConstFixedArray() cFixedArray32Compat {
|
||||
return cFixedArray32Compat(gmk.cFixedArray())
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
|
||||
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(gsp))
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) cConstFixedArray() cFixedArray289Compat {
|
||||
return cFixedArray289Compat(gsp.cFixedArray())
|
||||
}
|
||||
|
||||
func (gid *GroupIdentifier) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
|
||||
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gid))
|
||||
}
|
||||
|
||||
func (gid *GroupIdentifier) cConstFixedArray() cFixedArray32Compat {
|
||||
return cFixedArray32Compat(gid.cFixedArray())
|
||||
}
|
||||
|
||||
func (gid *GroupIdentifier) String() string {
|
||||
if gid == nil {
|
||||
return ""
|
||||
|
|
@ -84,8 +57,8 @@ func (gid *GroupIdentifier) String() string {
|
|||
return base64.StdEncoding.EncodeToString(gid[:])
|
||||
}
|
||||
|
||||
type UUIDCiphertext = fixedArray65
|
||||
type ProfileKeyCiphertext = fixedArray65
|
||||
type UUIDCiphertext [C.SignalUUID_CIPHERTEXT_LEN]byte
|
||||
type ProfileKeyCiphertext [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]byte
|
||||
|
||||
func GenerateGroupSecretParams() (GroupSecretParams, error) {
|
||||
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness())
|
||||
|
|
@ -103,48 +76,52 @@ func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
|
||||
return DeriveGroupSecretParamsFromMasterKey(gmk)
|
||||
}
|
||||
|
||||
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
|
||||
var params GroupSecretParams
|
||||
signalFfiError := C.signal_group_secret_params_generate_deterministic(params.cFixedArray(), randomness.cConstFixedArray())
|
||||
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_generate_deterministic(¶ms, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
|
||||
runtime.KeepAlive(randomness)
|
||||
if signalFfiError != nil {
|
||||
return GroupSecretParams{}, wrapError(signalFfiError)
|
||||
}
|
||||
return params, nil
|
||||
var groupSecretParams GroupSecretParams
|
||||
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(¶ms), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
|
||||
return groupSecretParams, nil
|
||||
}
|
||||
|
||||
func DeriveGroupSecretParamsFromMasterKey(groupMasterKey GroupMasterKey) (GroupSecretParams, error) {
|
||||
var params GroupSecretParams
|
||||
signalFfiError := C.signal_group_secret_params_derive_from_master_key(params.cFixedArray(), groupMasterKey.cConstFixedArray())
|
||||
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_derive_from_master_key(¶ms, (*[C.SignalGROUP_MASTER_KEY_LEN]C.uint8_t)(unsafe.Pointer(&groupMasterKey)))
|
||||
runtime.KeepAlive(groupMasterKey)
|
||||
if signalFfiError != nil {
|
||||
return GroupSecretParams{}, wrapError(signalFfiError)
|
||||
}
|
||||
return params, nil
|
||||
var groupSecretParams GroupSecretParams
|
||||
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(¶ms), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
|
||||
return groupSecretParams, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) GetPublicParams() (*GroupPublicParams, error) {
|
||||
var publicParams GroupPublicParams
|
||||
signalFfiError := C.signal_group_secret_params_get_public_params(publicParams.cFixedArray(), gsp.cConstFixedArray())
|
||||
var publicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_get_public_params(&publicParams, (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)))
|
||||
runtime.KeepAlive(gsp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &publicParams, nil
|
||||
var groupPublicParams GroupPublicParams
|
||||
copy(groupPublicParams[:], C.GoBytes(unsafe.Pointer(&publicParams), C.int(C.SignalGROUP_PUBLIC_PARAMS_LEN)))
|
||||
return &groupPublicParams, nil
|
||||
}
|
||||
|
||||
func GetGroupIdentifier(groupPublicParams GroupPublicParams) (*GroupIdentifier, error) {
|
||||
var groupIdentifier GroupIdentifier
|
||||
signalFfiError := C.signal_group_public_params_get_group_identifier(groupIdentifier.cFixedArray(), groupPublicParams.cConstFixedArray())
|
||||
var groupIdentifier [C.SignalGROUP_IDENTIFIER_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_public_params_get_group_identifier(&groupIdentifier, (*[C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(&groupPublicParams)))
|
||||
runtime.KeepAlive(groupPublicParams)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &groupIdentifier, nil
|
||||
var result GroupIdentifier
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&groupIdentifier), C.int(C.SignalGROUP_IDENTIFIER_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error) {
|
||||
|
|
@ -152,7 +129,7 @@ func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error
|
|||
borrowedBlob := BytesToBuffer(blob)
|
||||
signalFfiError := C.signal_group_secret_params_decrypt_blob_with_padding(
|
||||
&plaintext,
|
||||
gsp.cConstFixedArray(),
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
borrowedBlob,
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
|
|
@ -168,8 +145,8 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
|
|||
borrowedPlaintext := BytesToBuffer(plaintext)
|
||||
signalFfiError := C.signal_group_secret_params_encrypt_blob_with_padding_deterministic(
|
||||
&ciphertext,
|
||||
gsp.cConstFixedArray(),
|
||||
randomness.cConstFixedArray(),
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
|
||||
borrowedPlaintext,
|
||||
(C.uint32_t)(padding_len),
|
||||
)
|
||||
|
|
@ -184,11 +161,11 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
|
|||
}
|
||||
|
||||
func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
|
||||
var serviceIDBytes ServiceIDFixedBytes
|
||||
u := C.SignalServiceIdFixedWidthBinaryBytes{}
|
||||
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
|
||||
serviceIDBytes.cFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
ciphertextServiceID.cConstFixedArray(),
|
||||
&u,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(ciphertextServiceID)
|
||||
|
|
@ -196,31 +173,33 @@ func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertex
|
|||
return EmptyServiceID, wrapError(signalFfiError)
|
||||
}
|
||||
|
||||
serviceID := ServiceIDFromCFixedBytes(serviceIDBytes.cFixedArray())
|
||||
serviceID := ServiceIDFromCFixedBytes(&u)
|
||||
return serviceID, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) {
|
||||
var cipherTextServiceID UUIDCiphertext
|
||||
var cipherTextServiceID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
|
||||
cipherTextServiceID.cFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
serviceID.cConstFixedArray(),
|
||||
&cipherTextServiceID,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
serviceID.CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &cipherTextServiceID, nil
|
||||
var result UUIDCiphertext
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextServiceID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyCiphertext, u uuid.UUID) (*ProfileKey, error) {
|
||||
var profileKey ProfileKey
|
||||
profileKey := [C.SignalPROFILE_KEY_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_group_secret_params_decrypt_profile_key(
|
||||
profileKey.cFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
ciphertextProfileKey.cConstFixedArray(),
|
||||
NewACIServiceID(u).cConstFixedArray(),
|
||||
&profileKey,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextProfileKey)),
|
||||
NewACIServiceID(u).CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(ciphertextProfileKey)
|
||||
|
|
@ -228,23 +207,27 @@ func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyC
|
|||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &profileKey, nil
|
||||
var result ProfileKey
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&profileKey), C.int(C.SignalPROFILE_KEY_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) EncryptProfileKey(profileKey ProfileKey, u uuid.UUID) (*ProfileKeyCiphertext, error) {
|
||||
var ciphertextProfileKey ProfileKeyCiphertext
|
||||
ciphertextProfileKey := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_group_secret_params_encrypt_profile_key(
|
||||
ciphertextProfileKey.cFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
profileKey.cConstFixedArray(),
|
||||
NewACIServiceID(u).cConstFixedArray(),
|
||||
&ciphertextProfileKey,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalPROFILE_KEY_LEN]C.uint8_t)(unsafe.Pointer(&profileKey)),
|
||||
NewACIServiceID(u).CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(profileKey)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &ciphertextProfileKey, nil
|
||||
var result ProfileKeyCiphertext
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&ciphertextProfileKey), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp *ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
|
||||
|
|
@ -253,9 +236,9 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
|
|||
signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
|
||||
&out,
|
||||
C.SignalConstPointerServerPublicParams{spp},
|
||||
randomness.cConstFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
credential.cConstFixedArray(),
|
||||
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar)(unsafe.Pointer(&credential)),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(credential)
|
||||
|
|
@ -269,14 +252,16 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
|
|||
}
|
||||
|
||||
func (gsp *GroupSecretParams) GetMasterKey() (*GroupMasterKey, error) {
|
||||
var masterKey GroupMasterKey
|
||||
masterKeyBytes := [C.SignalGROUP_MASTER_KEY_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_group_secret_params_get_master_key(
|
||||
masterKey.cFixedArray(),
|
||||
gsp.cConstFixedArray(),
|
||||
&masterKeyBytes,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &masterKey, nil
|
||||
var groupMasterKey GroupMasterKey
|
||||
copy(groupMasterKey[:], C.GoBytes(unsafe.Pointer(&masterKeyBytes), C.int(C.SignalGROUP_MASTER_KEY_LEN)))
|
||||
return &groupMasterKey, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2025 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GroupSendFullToken []byte
|
||||
|
||||
func (gsft GroupSendFullToken) String() string {
|
||||
return base64.StdEncoding.EncodeToString(gsft)
|
||||
}
|
||||
|
||||
func (gsft GroupSendFullToken) CheckValidContents() error {
|
||||
signalFfiError := C.signal_group_send_full_token_check_valid_contents(
|
||||
BytesToBuffer(gsft),
|
||||
)
|
||||
runtime.KeepAlive(gsft)
|
||||
if signalFfiError != nil {
|
||||
return wrapError(signalFfiError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gsft GroupSendFullToken) GetExpiration() (time.Time, error) {
|
||||
var expiration C.uint64_t
|
||||
signalFfiError := C.signal_group_send_full_token_get_expiration(
|
||||
&expiration,
|
||||
BytesToBuffer(gsft),
|
||||
)
|
||||
runtime.KeepAlive(gsft)
|
||||
if signalFfiError != nil {
|
||||
return time.Time{}, wrapError(signalFfiError)
|
||||
}
|
||||
return time.Unix(int64(expiration), 0), nil
|
||||
}
|
||||
|
||||
type GroupSendToken []byte
|
||||
|
||||
func (gst GroupSendToken) CheckValidContents() error {
|
||||
signalFfiError := C.signal_group_send_token_check_valid_contents(
|
||||
BytesToBuffer(gst),
|
||||
)
|
||||
runtime.KeepAlive(gst)
|
||||
if signalFfiError != nil {
|
||||
return wrapError(signalFfiError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gst GroupSendToken) ToFullToken(expiration time.Time) (GroupSendFullToken, error) {
|
||||
var fullToken C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_group_send_token_to_full_token(
|
||||
&fullToken,
|
||||
BytesToBuffer(gst),
|
||||
C.uint64_t(expiration.Unix()),
|
||||
)
|
||||
runtime.KeepAlive(gst)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return CopySignalOwnedBufferToBytes(fullToken), nil
|
||||
}
|
||||
|
||||
type GroupSendEndorsement []byte
|
||||
|
||||
func (gse GroupSendEndorsement) ToToken(groupSecretParams *GroupSecretParams) (GroupSendToken, error) {
|
||||
var token C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_group_send_endorsement_to_token(
|
||||
&token,
|
||||
BytesToBuffer(gse),
|
||||
groupSecretParams.cConstFixedArray(),
|
||||
)
|
||||
runtime.KeepAlive(gse)
|
||||
runtime.KeepAlive(groupSecretParams)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return CopySignalOwnedBufferToBytes(token), nil
|
||||
}
|
||||
|
||||
func (gse GroupSendEndorsement) ToFullToken(params *GroupSecretParams, expiration time.Time) (GroupSendFullToken, error) {
|
||||
token, err := gse.ToToken(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token.ToFullToken(expiration)
|
||||
}
|
||||
|
||||
func (gse GroupSendEndorsement) CheckValidContents() error {
|
||||
signalFfiError := C.signal_group_send_endorsement_check_valid_contents(
|
||||
BytesToBuffer(gse),
|
||||
)
|
||||
runtime.KeepAlive(gse)
|
||||
if signalFfiError != nil {
|
||||
return wrapError(signalFfiError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gse GroupSendEndorsement) Remove(other GroupSendEndorsement) (GroupSendEndorsement, error) {
|
||||
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_group_send_endorsement_remove(
|
||||
&result,
|
||||
BytesToBuffer(gse),
|
||||
BytesToBuffer(other),
|
||||
)
|
||||
runtime.KeepAlive(gse)
|
||||
runtime.KeepAlive(other)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return CopySignalOwnedBufferToBytes(result), nil
|
||||
}
|
||||
|
||||
func GroupSendEndorsementCombine(endorsements ...GroupSendEndorsement) (GroupSendEndorsement, error) {
|
||||
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
cEndorsements, unpin := ManyBytesToBuffer(endorsements)
|
||||
defer unpin()
|
||||
signalFfiError := C.signal_group_send_endorsement_combine(
|
||||
&result,
|
||||
cEndorsements,
|
||||
)
|
||||
runtime.KeepAlive(endorsements)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return CopySignalOwnedBufferToBytes(result), nil
|
||||
}
|
||||
|
||||
type GroupSendEndorsementsResponse []byte
|
||||
|
||||
func (gser GroupSendEndorsementsResponse) GetExpiration() (time.Time, error) {
|
||||
var expiration C.uint64_t
|
||||
signalFfiError := C.signal_group_send_endorsements_response_get_expiration(
|
||||
&expiration,
|
||||
BytesToBuffer(gser),
|
||||
)
|
||||
runtime.KeepAlive(gser)
|
||||
if signalFfiError != nil {
|
||||
return time.Time{}, wrapError(signalFfiError)
|
||||
}
|
||||
return time.Unix(int64(expiration), 0), nil
|
||||
}
|
||||
|
||||
func (gser GroupSendEndorsementsResponse) CheckValidContents() error {
|
||||
signalFfiError := C.signal_group_send_endorsements_response_check_valid_contents(
|
||||
BytesToBuffer(gser),
|
||||
)
|
||||
runtime.KeepAlive(gser)
|
||||
if signalFfiError != nil {
|
||||
return wrapError(signalFfiError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gser GroupSendEndorsementsResponse) ReceiveWithServiceIDs(
|
||||
groupMembers []ServiceID, localUser ServiceID, params *GroupSecretParams, spp *ServerPublicParams,
|
||||
) (GroupSendEndorsement, map[ServiceID]GroupSendEndorsement, error) {
|
||||
var out C.SignalBytestringArray = C.SignalBytestringArray{}
|
||||
concatenatedMembers := make([]byte, len(groupMembers)*ServiceIDFixedBytesLength)
|
||||
for i, member := range groupMembers {
|
||||
copy(concatenatedMembers[i*ServiceIDFixedBytesLength:(i+1)*ServiceIDFixedBytesLength], member.FixedBytes()[:])
|
||||
}
|
||||
signalFfiError := C.signal_group_send_endorsements_response_receive_and_combine_with_service_ids(
|
||||
&out,
|
||||
BytesToBuffer(gser),
|
||||
BytesToBuffer(concatenatedMembers),
|
||||
localUser.cConstFixedArray(),
|
||||
C.uint64_t(time.Now().Unix()),
|
||||
params.cConstFixedArray(),
|
||||
C.SignalConstPointerServerPublicParams{spp},
|
||||
)
|
||||
runtime.KeepAlive(gser)
|
||||
runtime.KeepAlive(concatenatedMembers)
|
||||
runtime.KeepAlive(params)
|
||||
runtime.KeepAlive(spp)
|
||||
if signalFfiError != nil {
|
||||
return nil, nil, wrapError(signalFfiError)
|
||||
}
|
||||
endorsements := CopySignalBytestringArray[GroupSendEndorsement](out)
|
||||
memberEndorsements := make(map[ServiceID]GroupSendEndorsement, len(groupMembers))
|
||||
for i, member := range groupMembers {
|
||||
if len(endorsements) > i && len(endorsements[i]) > 0 {
|
||||
memberEndorsements[member] = endorsements[i]
|
||||
}
|
||||
}
|
||||
combined, err := GroupSendEndorsementCombine(endorsements...)
|
||||
if err != nil {
|
||||
return nil, memberEndorsements, err
|
||||
}
|
||||
return combined, memberEndorsements, nil
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ package libsignalgo
|
|||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
|
||||
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *keyp);
|
||||
typedef const SignalProtocolAddress const_address;
|
||||
typedef const SignalPublicKey const_public_key;
|
||||
|
||||
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPrivateKey **keyp);
|
||||
extern int signal_get_local_registration_id_callback(void *store_ctx, uint32_t *idp);
|
||||
extern int signal_save_identity_key_callback(void *store_ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
|
||||
extern int signal_get_identity_key_callback(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address);
|
||||
extern int signal_is_trusted_identity_callback(void *store_ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
|
||||
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
|
||||
extern int signal_save_identity_key_callback(void *store_ctx, const_address *address, const_public_key *public_key);
|
||||
extern int signal_get_identity_key_callback(void *store_ctx, SignalPublicKey **public_keyp, const_address *address);
|
||||
extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
|
|||
}
|
||||
|
||||
//export signal_get_identity_key_pair_callback
|
||||
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp *C.SignalPairOfMutPointerPrivateKeyMutPointerPublicKey) C.int {
|
||||
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp **C.SignalPrivateKey) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
key, err := store.GetIdentityKeyPair(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == nil {
|
||||
keyp.first.raw = nil
|
||||
keyp.second.raw = nil
|
||||
return nil
|
||||
}
|
||||
privClone, err := key.privateKey.Clone()
|
||||
*keyp = nil
|
||||
} else {
|
||||
clone, err := key.privateKey.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubClone, err := key.publicKey.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
clone.CancelFinalizer()
|
||||
*keyp = clone.ptr
|
||||
}
|
||||
privClone.CancelFinalizer()
|
||||
pubClone.CancelFinalizer()
|
||||
keyp.first.raw = privClone.ptr
|
||||
keyp.second.raw = pubClone.ptr
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
@ -88,17 +83,17 @@ func signal_get_local_registration_id_callback(storeCtx unsafe.Pointer, idp *C.u
|
|||
}
|
||||
|
||||
//export signal_save_identity_key_callback
|
||||
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, address C.SignalMutPointerProtocolAddress, publicKey C.SignalMutPointerPublicKey) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
publicKeyStruct := PublicKey{ptr: publicKey.raw}
|
||||
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, address *C.const_address, publicKey *C.const_public_key) C.int {
|
||||
return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
|
||||
publicKeyStruct := PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(publicKey))}
|
||||
cloned, err := publicKeyStruct.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
addr := &Address{ptr: address.raw}
|
||||
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
|
||||
theirServiceID, err := addr.NameServiceID()
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
replaced, err := store.SaveIdentityKey(
|
||||
ctx,
|
||||
|
|
@ -106,21 +101,20 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t,
|
|||
&IdentityKey{cloned},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
if replaced {
|
||||
*out = 1
|
||||
return 1, nil
|
||||
} else {
|
||||
*out = 0
|
||||
return 0, nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_get_identity_key_callback
|
||||
func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.SignalMutPointerPublicKey, address C.SignalMutPointerProtocolAddress) C.int {
|
||||
func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp **C.SignalPublicKey, address *C.const_address) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
addr := &Address{ptr: address.raw}
|
||||
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
|
||||
theirServiceID, err := addr.NameServiceID()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -128,42 +122,39 @@ func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.Si
|
|||
key, err := store.GetIdentityKey(ctx, theirServiceID)
|
||||
if err == nil && key != nil {
|
||||
key.publicKey.CancelFinalizer()
|
||||
public_keyp.raw = key.publicKey.ptr
|
||||
*public_keyp = key.publicKey.ptr
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_is_trusted_identity_callback
|
||||
func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, out *C.bool, address C.SignalMutPointerProtocolAddress, public_key C.SignalMutPointerPublicKey, direction C.uint32_t) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
addr := &Address{ptr: address.raw}
|
||||
func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, address *C.const_address, public_key *C.const_public_key, direction C.uint) C.int {
|
||||
return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
|
||||
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
|
||||
theirServiceID, err := addr.NameServiceID()
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: public_key.raw}}, SignalDirection(direction))
|
||||
trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(public_key))}}, SignalDirection(direction))
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
if trusted {
|
||||
return 1, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
*out = C.bool(trusted)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_destroy_identity_key_store_callback
|
||||
func signal_destroy_identity_key_store_callback(storeCtx unsafe.Pointer) {
|
||||
// No-op: Go's garbage collector handles cleanup
|
||||
}
|
||||
|
||||
func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct {
|
||||
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
|
||||
ctx: wrapStore(ctx, store),
|
||||
get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback),
|
||||
get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
|
||||
get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback),
|
||||
save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback),
|
||||
is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback),
|
||||
destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
|
||||
get_identity_key_pair: C.SignalGetIdentityKeyPair(C.signal_get_identity_key_pair_callback),
|
||||
get_local_registration_id: C.SignalGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
|
||||
save_identity: C.SignalSaveIdentityKey(C.signal_save_identity_key_callback),
|
||||
get_identity: C.SignalGetIdentityKey(C.signal_get_identity_key_callback),
|
||||
is_trusted_identity: C.SignalIsTrustedIdentity(C.signal_is_trusted_identity_callback),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ package libsignalgo
|
|||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
|
||||
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id);
|
||||
extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
|
||||
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
|
||||
extern void signal_destroy_kyber_pre_key_store_callback(void *store_ctx);
|
||||
typedef const SignalKyberPreKeyRecord const_kyber_pre_key_record;
|
||||
|
||||
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalKyberPreKeyRecord **recordp, uint32_t id);
|
||||
extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, const_kyber_pre_key_record *record);
|
||||
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -38,21 +39,21 @@ type KyberPreKeyStore interface {
|
|||
}
|
||||
|
||||
//export signal_load_kyber_pre_key_callback
|
||||
func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerKyberPreKeyRecord, id C.uint32_t) C.int {
|
||||
func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalKyberPreKeyRecord, id C.uint32_t) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
|
||||
key, err := store.LoadKyberPreKey(ctx, uint32(id))
|
||||
if err == nil && key != nil {
|
||||
key.CancelFinalizer()
|
||||
keyp.raw = key.ptr
|
||||
*keyp = key.ptr
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_store_kyber_pre_key_callback
|
||||
func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerKyberPreKeyRecord) C.int {
|
||||
func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_kyber_pre_key_record) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
|
||||
record := KyberPreKeyRecord{ptr: preKeyRecord.raw}
|
||||
record := KyberPreKeyRecord{ptr: (*C.SignalKyberPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
|
||||
cloned, err := record.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -62,24 +63,18 @@ func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t,
|
|||
}
|
||||
|
||||
//export signal_mark_kyber_pre_key_used_callback
|
||||
func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t, ecPrekeyID C.uint32_t, baseKey C.SignalMutPointerPublicKey) C.int {
|
||||
func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
|
||||
// TODO use ecPrekeyID and baseKey?
|
||||
return store.MarkKyberPreKeyUsed(ctx, uint32(id))
|
||||
err := store.MarkKyberPreKeyUsed(ctx, uint32(id))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_destroy_kyber_pre_key_store_callback
|
||||
func signal_destroy_kyber_pre_key_store_callback(storeCtx unsafe.Pointer) {
|
||||
// No-op: Go's garbage collector handles cleanup
|
||||
}
|
||||
|
||||
func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
|
||||
return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
|
||||
ctx: wrapStore(ctx, store),
|
||||
load_kyber_pre_key: C.SignalFfiKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
|
||||
store_kyber_pre_key: C.SignalFfiKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
|
||||
mark_kyber_pre_key_used: C.SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
|
||||
destroy: C.SignalFfiKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
|
||||
load_kyber_pre_key: C.SignalLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
|
||||
store_kyber_pre_key: C.SignalStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
|
||||
mark_kyber_pre_key_used: C.SignalMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 857c4dca03537dc5e395a5e1eda6bf18f59c3601
|
||||
Subproject commit c02859f57552477680fb7928c3c3426193040313
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -19,9 +19,8 @@ package libsignalgo
|
|||
/*
|
||||
#include <./libsignal-ffi.h>
|
||||
|
||||
extern void signal_log_callback(void *ctx, SignalLogLevel level, SignalCStringPtr file, uint32_t line, SignalCStringPtr message);
|
||||
extern void signal_log_callback(void *ctx, SignalLogLevel level, char *file, uint32_t line, char *message);
|
||||
extern void signal_log_flush_callback(void *ctx);
|
||||
extern void signal_log_destroy_callback(void *ctx);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -32,8 +31,8 @@ import (
|
|||
var ffiLogger Logger
|
||||
|
||||
//export signal_log_callback
|
||||
func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file C.SignalCStringPtr, line C.uint32_t, message C.SignalCStringPtr) {
|
||||
ffiLogger.Log(LogLevel(int(level)), CopyCStringToString(file), uint(line), CopyCStringToString(message))
|
||||
func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file *C.char, line C.uint32_t, message *C.char) {
|
||||
ffiLogger.Log(LogLevel(int(level)), C.GoString(file), uint(line), C.GoString(message))
|
||||
}
|
||||
|
||||
//export signal_log_flush_callback
|
||||
|
|
@ -41,11 +40,6 @@ func signal_log_flush_callback(ctx unsafe.Pointer) {
|
|||
ffiLogger.Flush()
|
||||
}
|
||||
|
||||
//export signal_log_destroy_callback
|
||||
func signal_log_destroy_callback(ctx unsafe.Pointer) {
|
||||
ffiLogger.Destroy()
|
||||
}
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
|
|
@ -59,14 +53,12 @@ const (
|
|||
type Logger interface {
|
||||
Log(level LogLevel, file string, line uint, message string)
|
||||
Flush()
|
||||
Destroy()
|
||||
}
|
||||
|
||||
func InitLogger(level LogLevel, logger Logger) {
|
||||
ffiLogger = logger
|
||||
C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLoggerStruct{
|
||||
log: C.SignalFfiLoggerLog(C.signal_log_callback),
|
||||
flush: C.SignalFfiLoggerFlush(C.signal_log_flush_callback),
|
||||
destroy: C.SignalFfiLoggerDestroy(C.signal_log_destroy_callback),
|
||||
C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLogger{
|
||||
log: C.SignalLogCallback(C.signal_log_callback),
|
||||
flush: C.SignalLogFlushCallback(C.signal_log_flush_callback),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) {
|
||||
func Encrypt(ctx context.Context, plaintext []byte, forAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) {
|
||||
var ciphertextMessage C.SignalMutPointerCiphertextMessage
|
||||
var now C.uint64_t = C.uint64_t(time.Now().Unix())
|
||||
callbackCtx := NewCallbackContext(ctx)
|
||||
|
|
@ -36,7 +36,6 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
|
|||
&ciphertextMessage,
|
||||
BytesToBuffer(plaintext),
|
||||
forAddress.constPtr(),
|
||||
localAddress.constPtr(),
|
||||
callbackCtx.wrapSessionStore(sessionStore),
|
||||
callbackCtx.wrapIdentityKeyStore(identityKeyStore),
|
||||
now,
|
||||
|
|
@ -49,7 +48,7 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
|
|||
return wrapCiphertextMessage(ciphertextMessage.raw), nil
|
||||
}
|
||||
|
||||
func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
|
||||
func Decrypt(ctx context.Context, message *Message, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
|
||||
callbackCtx := NewCallbackContext(ctx)
|
||||
defer callbackCtx.Unref()
|
||||
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
|
|
@ -57,7 +56,6 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
|
|||
&decrypted,
|
||||
message.constPtr(),
|
||||
fromAddress.constPtr(),
|
||||
localAddress.constPtr(),
|
||||
callbackCtx.wrapSessionStore(sessionStore),
|
||||
callbackCtx.wrapIdentityKeyStore(identityStore),
|
||||
)
|
||||
|
|
@ -71,10 +69,10 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
|
|||
|
||||
type Message struct {
|
||||
nc noCopy
|
||||
ptr *C.SignalSignalMessage
|
||||
ptr *C.SignalMessage
|
||||
}
|
||||
|
||||
func wrapMessage(ptr *C.SignalSignalMessage) *Message {
|
||||
func wrapMessage(ptr *C.SignalMessage) *Message {
|
||||
message := &Message{ptr: ptr}
|
||||
runtime.SetFinalizer(message, (*Message).Destroy)
|
||||
return message
|
||||
|
|
@ -156,3 +154,22 @@ func (m *Message) GetCounter() (uint32, error) {
|
|||
}
|
||||
return uint32(counter), nil
|
||||
}
|
||||
|
||||
func (m *Message) VerifyMAC(sender, receiver *PublicKey, macKey []byte) (bool, error) {
|
||||
var result C.bool
|
||||
signalFfiError := C.signal_message_verify_mac(
|
||||
&result,
|
||||
m.constPtr(),
|
||||
sender.constPtr(),
|
||||
receiver.constPtr(),
|
||||
BytesToBuffer(macKey),
|
||||
)
|
||||
runtime.KeepAlive(m)
|
||||
runtime.KeepAlive(sender)
|
||||
runtime.KeepAlive(receiver)
|
||||
runtime.KeepAlive(macKey)
|
||||
if signalFfiError != nil {
|
||||
return false, wrapError(signalFfiError)
|
||||
}
|
||||
return bool(result), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,16 @@ package libsignalgo
|
|||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import "runtime"
|
||||
import (
|
||||
"runtime"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type MessageBackupKey struct {
|
||||
nc noCopy
|
||||
ptr *C.SignalMessageBackupKey
|
||||
}
|
||||
|
||||
const MessageBackupKeyBytesLength = 32
|
||||
|
||||
type messageBackupKeyBytes = fixedArray32
|
||||
|
||||
func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
|
||||
backupKey := &MessageBackupKey{ptr: ptr}
|
||||
runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy)
|
||||
|
|
@ -39,12 +38,10 @@ func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
|
|||
|
||||
func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceID) (*MessageBackupKey, error) {
|
||||
var bk C.SignalMutPointerMessageBackupKey
|
||||
aepC, free := GoStringToCString(string(aep))
|
||||
defer free()
|
||||
signalFfiError := C.signal_message_backup_key_from_account_entropy_pool(
|
||||
&bk,
|
||||
aepC,
|
||||
aci.cConstFixedArray(),
|
||||
C.CString(string(aep)),
|
||||
aci.CFixedBytes(),
|
||||
nil, // TODO what's a forward secrecy token?
|
||||
)
|
||||
runtime.KeepAlive(aep)
|
||||
|
|
@ -58,8 +55,8 @@ func MessageBackupKeyFromBackupKeyAndID(backupKey *BackupKey, backupID *BackupID
|
|||
var bk C.SignalMutPointerMessageBackupKey
|
||||
signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id(
|
||||
&bk,
|
||||
backupKey.cConstFixedArray(),
|
||||
backupID.cConstFixedArray(),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(backupKey)),
|
||||
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(backupID)),
|
||||
nil, // TODO what's a forward secrecy token?
|
||||
)
|
||||
runtime.KeepAlive(backupKey)
|
||||
|
|
@ -83,26 +80,26 @@ func (bk *MessageBackupKey) Destroy() error {
|
|||
return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr()))
|
||||
}
|
||||
|
||||
func (bk *MessageBackupKey) GetHMACKey() ([MessageBackupKeyBytesLength]byte, error) {
|
||||
var out messageBackupKeyBytes
|
||||
func (bk *MessageBackupKey) GetHMACKey() ([32]byte, error) {
|
||||
var out [32]byte
|
||||
signalFfiError := C.signal_message_backup_key_get_hmac_key(
|
||||
out.cFixedArray(),
|
||||
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
bk.constPtr(),
|
||||
)
|
||||
if signalFfiError != nil {
|
||||
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
|
||||
return out, wrapError(signalFfiError)
|
||||
}
|
||||
return [MessageBackupKeyBytesLength]byte(out), nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (bk *MessageBackupKey) GetAESKey() ([MessageBackupKeyBytesLength]byte, error) {
|
||||
var out messageBackupKeyBytes
|
||||
func (bk *MessageBackupKey) GetAESKey() ([32]byte, error) {
|
||||
var out [32]byte
|
||||
signalFfiError := C.signal_message_backup_key_get_aes_key(
|
||||
out.cFixedArray(),
|
||||
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
bk.constPtr(),
|
||||
)
|
||||
if signalFfiError != nil {
|
||||
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
|
||||
return out, wrapError(signalFfiError)
|
||||
}
|
||||
return [MessageBackupKeyBytesLength]byte(out), nil
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ package libsignalgo
|
|||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
|
@ -32,38 +31,12 @@ import (
|
|||
"go.mau.fi/util/random"
|
||||
)
|
||||
|
||||
const ProfileKeyLength = 32
|
||||
const AccessKeyLength = 16
|
||||
const ProfileKeyVersionLength = 64
|
||||
const ProfileKeyLength = C.SignalPROFILE_KEY_LEN
|
||||
|
||||
type ProfileKey [ProfileKeyLength]byte
|
||||
type ProfileKeyCommitment = fixedArray97
|
||||
type ProfileKeyVersion [ProfileKeyVersionLength]byte
|
||||
type AccessKey [AccessKeyLength]byte
|
||||
|
||||
func (pk *ProfileKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
|
||||
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(pk))
|
||||
}
|
||||
|
||||
func (pk *ProfileKey) cConstFixedArray() cFixedArray32Compat {
|
||||
return cFixedArray32Compat(pk.cFixedArray())
|
||||
}
|
||||
|
||||
func (pkv *ProfileKeyVersion) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
|
||||
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(pkv))
|
||||
}
|
||||
|
||||
func (pkv *ProfileKeyVersion) cConstFixedArray() cFixedArray64Compat {
|
||||
return cFixedArray64Compat(pkv.cFixedArray())
|
||||
}
|
||||
|
||||
func (ak *AccessKey) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
|
||||
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(ak))
|
||||
}
|
||||
|
||||
func (ak *AccessKey) cConstFixedArray() cFixedArray16Compat {
|
||||
return cFixedArray16Compat(ak.cFixedArray())
|
||||
}
|
||||
type ProfileKeyCommitment [C.SignalPROFILE_KEY_COMMITMENT_LEN]byte
|
||||
type ProfileKeyVersion [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]byte
|
||||
type AccessKey [C.SignalACCESS_KEY_LEN]byte
|
||||
|
||||
func DeserializeProfileKey(bytes []byte) (*ProfileKey, error) {
|
||||
if len(bytes) == 0 {
|
||||
|
|
@ -81,6 +54,10 @@ func (pk *ProfileKey) IsEmpty() bool {
|
|||
return pk == nil || *pk == blankProfileKey
|
||||
}
|
||||
|
||||
func (ak *AccessKey) String() string {
|
||||
return string(ak[:])
|
||||
}
|
||||
|
||||
func (pv *ProfileKeyVersion) String() string {
|
||||
return string(pv[:])
|
||||
}
|
||||
|
|
@ -92,30 +69,14 @@ func (pk *ProfileKey) Slice() []byte {
|
|||
return pk[:]
|
||||
}
|
||||
|
||||
func (ak *AccessKey) Xor(other *AccessKey) *AccessKey {
|
||||
if ak == nil {
|
||||
return other
|
||||
} else if other == nil {
|
||||
return ak
|
||||
}
|
||||
var result AccessKey
|
||||
for i := 0; i < AccessKeyLength; i++ {
|
||||
result[i] = ak[i] ^ other[i]
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
func (ak *AccessKey) String() string {
|
||||
return base64.StdEncoding.EncodeToString(ak[:])
|
||||
}
|
||||
|
||||
func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) {
|
||||
var result ProfileKeyCommitment
|
||||
c_uuid := NewACIServiceID(u).cConstFixedArray()
|
||||
c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{}
|
||||
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
|
||||
c_uuid := NewACIServiceID(u).CFixedBytes()
|
||||
|
||||
signalFfiError := C.signal_profile_key_get_commitment(
|
||||
result.cFixedArray(),
|
||||
pk.cConstFixedArray(),
|
||||
&c_result,
|
||||
c_profileKey,
|
||||
c_uuid,
|
||||
)
|
||||
runtime.KeepAlive(pk)
|
||||
|
|
@ -125,16 +86,19 @@ func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error)
|
|||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
||||
var result ProfileKeyCommitment
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_COMMITMENT_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, error) {
|
||||
var result ProfileKeyVersion
|
||||
c_uuid := NewACIServiceID(u).cConstFixedArray()
|
||||
c_result := [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]C.uchar{}
|
||||
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
|
||||
c_uuid := NewACIServiceID(u).CFixedBytes()
|
||||
|
||||
signalFfiError := C.signal_profile_key_get_profile_key_version(
|
||||
result.cFixedArray(),
|
||||
pk.cConstFixedArray(),
|
||||
&c_result,
|
||||
c_profileKey,
|
||||
c_uuid,
|
||||
)
|
||||
runtime.KeepAlive(pk)
|
||||
|
|
@ -144,15 +108,18 @@ func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, err
|
|||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
||||
var result ProfileKeyVersion
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_VERSION_ENCODED_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
|
||||
var result AccessKey
|
||||
c_result := [C.SignalACCESS_KEY_LEN]C.uchar{}
|
||||
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
|
||||
|
||||
signalFfiError := C.signal_profile_key_derive_access_key(
|
||||
result.cFixedArray(),
|
||||
pk.cConstFixedArray(),
|
||||
&c_result,
|
||||
c_profileKey,
|
||||
)
|
||||
runtime.KeepAlive(pk)
|
||||
|
||||
|
|
@ -160,35 +127,31 @@ func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
|
|||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
||||
var result AccessKey
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalACCESS_KEY_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
type ProfileKeyCredentialRequestContext [473]byte
|
||||
type ProfileKeyCredentialRequest = fixedArray329
|
||||
type ProfileKeyCredentialRequestContext [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]byte
|
||||
type ProfileKeyCredentialRequest [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]byte
|
||||
type ProfileKeyCredentialResponse []byte
|
||||
type ProfileKeyCredentialPresentation []byte
|
||||
type ExpiringProfileKeyCredential = fixedArray153
|
||||
type ExpiringProfileKeyCredentialResponse = fixedArray497
|
||||
|
||||
func (p *ProfileKeyCredentialRequestContext) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
|
||||
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(p))
|
||||
}
|
||||
|
||||
func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() cFixedArray473Compat {
|
||||
return cFixedArray473Compat(p.cFixedArray())
|
||||
}
|
||||
type ExpiringProfileKeyCredential [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]byte
|
||||
type ExpiringProfileKeyCredentialResponse [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]byte
|
||||
|
||||
func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) {
|
||||
var result ProfileKeyCredentialRequestContext
|
||||
randBytes := Randomness(random.Bytes(RandomnessLength))
|
||||
c_uuid := NewACIServiceID(u).cConstFixedArray()
|
||||
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar{}
|
||||
randBytes := [32]byte(random.Bytes(32))
|
||||
c_random := (*[32]C.uchar)(unsafe.Pointer(&randBytes[0]))
|
||||
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(&profileKey[0]))
|
||||
c_uuid := NewACIServiceID(u).CFixedBytes()
|
||||
|
||||
signalFfiError := C.signal_server_public_params_create_profile_key_credential_request_context_deterministic(
|
||||
result.cFixedArray(),
|
||||
&c_result,
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
randBytes.cConstFixedArray(),
|
||||
c_random,
|
||||
c_uuid,
|
||||
profileKey.cConstFixedArray(),
|
||||
c_profileKey,
|
||||
)
|
||||
runtime.KeepAlive(u)
|
||||
runtime.KeepAlive(profileKey)
|
||||
|
|
@ -196,20 +159,23 @@ func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicPa
|
|||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
result := ProfileKeyCredentialRequestContext(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (p *ProfileKeyCredentialRequestContext) ProfileKeyCredentialRequestContextGetRequest() (*ProfileKeyCredentialRequest, error) {
|
||||
var result ProfileKeyCredentialRequest
|
||||
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]C.uchar{}
|
||||
c_context := (*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(p))
|
||||
|
||||
signalFfiError := C.signal_profile_key_credential_request_context_get_request(
|
||||
result.cFixedArray(),
|
||||
p.cConstFixedArray(),
|
||||
&c_result,
|
||||
c_context,
|
||||
)
|
||||
runtime.KeepAlive(p)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
result := ProfileKeyCredentialRequest(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
|
@ -225,12 +191,12 @@ func NewExpiringProfileKeyCredentialResponse(b []byte) (*ExpiringProfileKeyCrede
|
|||
}
|
||||
|
||||
func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext *ProfileKeyCredentialRequestContext, response *ExpiringProfileKeyCredentialResponse, currentTimeInSeconds uint64) (*ExpiringProfileKeyCredential, error) {
|
||||
var credential ExpiringProfileKeyCredential
|
||||
c_credential := [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_server_public_params_receive_expiring_profile_key_credential(
|
||||
credential.cFixedArray(),
|
||||
&c_credential,
|
||||
C.SignalConstPointerServerPublicParams{spp},
|
||||
requestContext.cConstFixedArray(),
|
||||
response.cConstFixedArray(),
|
||||
(*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(requestContext)),
|
||||
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]C.uchar)(unsafe.Pointer(response)),
|
||||
(C.uint64_t)(currentTimeInSeconds),
|
||||
)
|
||||
runtime.KeepAlive(requestContext)
|
||||
|
|
@ -239,6 +205,8 @@ func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext
|
|||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
credential := ExpiringProfileKeyCredential{}
|
||||
copy(credential[:], C.GoBytes(unsafe.Pointer(&c_credential), C.int(C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN)))
|
||||
return &credential, nil
|
||||
}
|
||||
|
||||
|
|
@ -249,21 +217,25 @@ func (a ProfileKeyCredentialPresentation) CheckValidContents() error {
|
|||
}
|
||||
|
||||
func (a ProfileKeyCredentialPresentation) UUIDCiphertext() (UUIDCiphertext, error) {
|
||||
var out UUIDCiphertext
|
||||
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(out.cFixedArray(), BytesToBuffer(a))
|
||||
out := [C.SignalUUID_CIPHERTEXT_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(&out, BytesToBuffer(a))
|
||||
runtime.KeepAlive(a)
|
||||
if signalFfiError != nil {
|
||||
return UUIDCiphertext{}, wrapError(signalFfiError)
|
||||
}
|
||||
return out, nil
|
||||
var result UUIDCiphertext
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a ProfileKeyCredentialPresentation) ProfileKeyCiphertext() (ProfileKeyCiphertext, error) {
|
||||
var out ProfileKeyCiphertext
|
||||
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(out.cFixedArray(), BytesToBuffer(a))
|
||||
out := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
|
||||
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(&out, BytesToBuffer(a))
|
||||
runtime.KeepAlive(a)
|
||||
if signalFfiError != nil {
|
||||
return ProfileKeyCiphertext{}, wrapError(signalFfiError)
|
||||
}
|
||||
return out, nil
|
||||
var result ProfileKeyCiphertext
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -44,14 +44,10 @@ func wrapSenderCertificate(ptr *C.SignalSenderCertificate) *SenderCertificate {
|
|||
// the Swift bindings).
|
||||
func NewSenderCertificate(sender *SealedSenderAddress, publicKey *PublicKey, expiration time.Time, signerCertificate *ServerCertificate, signerKey *PrivateKey) (*SenderCertificate, error) {
|
||||
var sc C.SignalMutPointerSenderCertificate
|
||||
senderUUIDStr, freeSenderUUIDStr := GoStringToCString(sender.UUID.String())
|
||||
defer freeSenderUUIDStr()
|
||||
senderE164Str, freeSenderE164Str := GoStringToCString(sender.E164)
|
||||
defer freeSenderE164Str()
|
||||
signalFfiError := C.signal_sender_certificate_new(
|
||||
&sc,
|
||||
senderUUIDStr,
|
||||
senderE164Str,
|
||||
C.CString(sender.UUID.String()),
|
||||
C.CString(sender.E164),
|
||||
C.uint32_t(sender.DeviceID),
|
||||
publicKey.constPtr(),
|
||||
C.uint64_t(expiration.UnixMilli()),
|
||||
|
|
@ -139,7 +135,7 @@ func (sc *SenderCertificate) GetSignature() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, error) {
|
||||
var rawUUID C.SignalCStringPtr
|
||||
var rawUUID *C.char
|
||||
signalFfiError := C.signal_sender_certificate_get_sender_uuid(&rawUUID, sc.constPtr())
|
||||
runtime.KeepAlive(sc)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -149,7 +145,7 @@ func (sc *SenderCertificate) GetSenderUUID() (uuid.UUID, error) {
|
|||
}
|
||||
|
||||
func (sc *SenderCertificate) GetSenderE164() (string, error) {
|
||||
var e164 C.SignalCStringPtr
|
||||
var e164 *C.char
|
||||
signalFfiError := C.signal_sender_certificate_get_sender_e164(&e164, sc.constPtr())
|
||||
runtime.KeepAlive(sc)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,15 @@ import "C"
|
|||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type ServerPublicParams = C.SignalServerPublicParams
|
||||
type NotarySignature = fixedArray64
|
||||
|
||||
const ServerPublicParamsLength = 673
|
||||
type NotarySignature [C.SignalSIGNATURE_LEN]byte
|
||||
|
||||
func DeserializeServerPublicParams(params []byte) (*ServerPublicParams, error) {
|
||||
if len(params) != ServerPublicParamsLength {
|
||||
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), ServerPublicParamsLength)
|
||||
if len(params) != C.SignalSERVER_PUBLIC_PARAMS_LEN {
|
||||
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), int(C.SignalSERVER_PUBLIC_PARAMS_LEN))
|
||||
}
|
||||
var out C.SignalMutPointerServerPublicParams
|
||||
signalFfiError := C.signal_server_public_params_deserialize(&out, BytesToBuffer(params[:]))
|
||||
|
|
@ -48,10 +47,11 @@ func ServerPublicParamsVerifySignature(
|
|||
messageBytes []byte,
|
||||
NotarySignature NotarySignature,
|
||||
) error {
|
||||
c_notarySignature := (*[C.SignalSIGNATURE_LEN]C.uint8_t)(unsafe.Pointer(&NotarySignature[0]))
|
||||
signalFfiError := C.signal_server_public_params_verify_signature(
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
BytesToBuffer(messageBytes),
|
||||
NotarySignature.cConstFixedArray(),
|
||||
c_notarySignature,
|
||||
)
|
||||
runtime.KeepAlive(messageBytes)
|
||||
return wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if C.SignalUUID_LEN != 16 {
|
||||
panic("libsignal-ffi uuid type size mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceIDType byte
|
||||
|
||||
const (
|
||||
|
|
@ -87,9 +93,6 @@ func (s ServiceID) IsEmpty() bool {
|
|||
}
|
||||
|
||||
func (s ServiceID) Address(deviceID uint) (*Address, error) {
|
||||
if s.IsEmpty() {
|
||||
return nil, fmt.Errorf("cannot create address from empty ServiceID")
|
||||
}
|
||||
return newAddress(s.String(), deviceID)
|
||||
}
|
||||
|
||||
|
|
@ -115,28 +118,12 @@ func (s ServiceID) GoString() string {
|
|||
return fmt.Sprintf(`libsignalgo.ServiceID{Type: %#v, UUID: uuid.MustParse("%s")}`, s.Type, s.UUID)
|
||||
}
|
||||
|
||||
func (s ServiceID) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s *ServiceID) UnmarshalText(text []byte) error {
|
||||
parsed, err := ServiceIDFromString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ServiceID) MarshalZerologObject(e *zerolog.Event) {
|
||||
e.Stringer("type", s.Type)
|
||||
e.Stringer("uuid", s.UUID)
|
||||
}
|
||||
|
||||
const ServiceIDUUIDLength = 16
|
||||
const ServiceIDFixedBytesLength = 17
|
||||
|
||||
type ServiceIDFixedBytes = fixedArray17
|
||||
type ServiceIDFixedBytes [17]byte
|
||||
|
||||
func (s ServiceID) FixedBytes() *ServiceIDFixedBytes {
|
||||
var result ServiceIDFixedBytes
|
||||
|
|
@ -164,19 +151,7 @@ func ServiceIDFromString(val string) (ServiceID, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) {
|
||||
if len(bytes) == ServiceIDUUIDLength {
|
||||
return NewACIServiceID(uuid.UUID(bytes)), nil
|
||||
} else if len(bytes) == ServiceIDFixedBytesLength {
|
||||
return ServiceID{
|
||||
Type: ServiceIDType(bytes[0]),
|
||||
UUID: uuid.UUID(bytes[1:]),
|
||||
}, nil
|
||||
}
|
||||
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected %d or %d)", len(bytes), ServiceIDUUIDLength, ServiceIDFixedBytesLength)
|
||||
}
|
||||
|
||||
func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) ServiceID {
|
||||
func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
|
||||
var id ServiceID
|
||||
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))
|
||||
id.Type = ServiceIDType(fixedBytes[0])
|
||||
|
|
@ -184,10 +159,6 @@ func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) Serv
|
|||
return id
|
||||
}
|
||||
|
||||
func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
|
||||
return s.FixedBytes().cFixedArray()
|
||||
}
|
||||
|
||||
func (s ServiceID) cConstFixedArray() cFixedArray17Compat {
|
||||
return cFixedArray17Compat(s.cFixedArray())
|
||||
func (s ServiceID) CFixedBytes() cPNIType {
|
||||
return cPNIType(unsafe.Pointer(s.FixedBytes()))
|
||||
}
|
||||
|
|
|
|||
11
pkg/libsignalgo/serviceid_clang.go
Normal file
11
pkg/libsignalgo/serviceid_clang.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//go:build darwin || android || ios || (windows && arm64)
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type cPNIType = *C.SignalServiceIdFixedWidthBinaryBytes
|
||||
14
pkg/libsignalgo/serviceid_gcc.go
Normal file
14
pkg/libsignalgo/serviceid_gcc.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//go:build !(darwin || android || ios || (windows && arm64))
|
||||
|
||||
package libsignalgo
|
||||
|
||||
/*
|
||||
#include "./libsignal-ffi.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// Hack for https://github.com/golang/go/issues/7270
|
||||
// The clang version is more correct, but doesn't work with gcc
|
||||
|
||||
type cPNIType = *[17]C.uint8_t
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#!/bin/sh
|
||||
cd /data
|
||||
export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER=""
|
||||
apk add --no-cache git make cmake protobuf-dev musl-dev g++ clang-dev
|
||||
apk add --no-cache git make cmake protoc musl-dev g++ clang-dev cbindgen
|
||||
cd libsignal
|
||||
cargo build -p libsignal-ffi --release
|
||||
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
|
||||
cd ..
|
||||
mv libsignal/target/release/libsignal_ffi.a .
|
||||
cp libsignal/swift/Sources/SignalFfi/signal_ffi.h libsignal-ffi.h
|
||||
mv libsignal/libsignal-ffi.h .
|
||||
chown 1000:1000 libsignal_ffi.a libsignal-ffi.h version.go
|
||||
|
|
|
|||
|
|
@ -28,11 +28,14 @@ echo "const Version = \"$(git describe --tags --always)\"" >> ../version.go
|
|||
# Build libsignal
|
||||
cargo build -p libsignal-ffi --release
|
||||
|
||||
# Regenerate the header file
|
||||
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
|
||||
|
||||
# Navigate back to the original directory
|
||||
cd "$ORIGINAL_DIR"
|
||||
|
||||
# Copy files from the libsignal directory
|
||||
cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" .
|
||||
cp "${LIBSIGNAL_DIRECTORY}/swift/Sources/SignalFfi/signal_ffi.h" libsignal-ffi.h
|
||||
cp "${LIBSIGNAL_DIRECTORY}/libsignal-ffi.h" .
|
||||
|
||||
echo "Files copied successfully."
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
package libsignalgo
|
||||
|
||||
const Version = "v0.100.0"
|
||||
const Version = "v0.84.0"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ func (mc *MessageConverter) ToSignal(
|
|||
portal *bridgev2.Portal,
|
||||
evt *event.Event,
|
||||
content *event.MessageEventContent,
|
||||
timestamp uint64,
|
||||
relaybotFormatted bool,
|
||||
replyTo *database.Message,
|
||||
) (*signalpb.DataMessage, error) {
|
||||
|
|
@ -54,6 +55,7 @@ func (mc *MessageConverter) ToSignal(
|
|||
}
|
||||
|
||||
dm := &signalpb.DataMessage{
|
||||
Timestamp: ×tamp,
|
||||
Preview: mc.convertURLPreviewToSignal(ctx, content),
|
||||
}
|
||||
if replyTo != nil {
|
||||
|
|
@ -61,7 +63,7 @@ func (mc *MessageConverter) ToSignal(
|
|||
if err == nil {
|
||||
dm.Quote = &signalpb.DataMessage_Quote{
|
||||
Id: proto.Uint64(messageID),
|
||||
AuthorAciBinary: authorACI[:],
|
||||
AuthorAci: proto.String(authorACI.String()),
|
||||
Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
|
||||
}
|
||||
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
|
||||
|
|
@ -110,9 +112,6 @@ func (mc *MessageConverter) ToSignal(
|
|||
return nil, fmt.Errorf("failed to convert sticker: %w", err)
|
||||
}
|
||||
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
|
||||
|
||||
dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker)
|
||||
if dm.Sticker == nil {
|
||||
var emoji *string
|
||||
// TODO check for single grapheme cluster?
|
||||
if len([]rune(content.Body)) == 1 {
|
||||
|
|
@ -124,10 +123,10 @@ func (mc *MessageConverter) ToSignal(
|
|||
PackId: make([]byte, 16),
|
||||
PackKey: make([]byte, 32),
|
||||
StickerId: proto.Uint32(0),
|
||||
|
||||
Data: att,
|
||||
Emoji: emoji,
|
||||
}
|
||||
}
|
||||
dm.Sticker.Data = att
|
||||
case event.MsgLocation:
|
||||
lat, lon, err := parseGeoURI(content.GeoURI)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,7 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -55,7 +51,7 @@ func calculateLength(dm *signalpb.DataMessage) int {
|
|||
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
||||
return 1
|
||||
}
|
||||
if dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil {
|
||||
if dm.Sticker != nil {
|
||||
return 1
|
||||
}
|
||||
length := len(dm.Attachments) + len(dm.Contact)
|
||||
|
|
@ -79,13 +75,11 @@ func CanConvertSignal(dm *signalpb.DataMessage) bool {
|
|||
}
|
||||
|
||||
const ViewOnceDisappearTimer = 5 * time.Minute
|
||||
const matrixTextMaxLength = 30000 // approximate value to avoid hitting 64 KiB PDU size limit with HTML duplication
|
||||
|
||||
func (mc *MessageConverter) ToMatrix(
|
||||
ctx context.Context,
|
||||
client *signalmeow.Client,
|
||||
portal *bridgev2.Portal,
|
||||
sender uuid.UUID,
|
||||
intent bridgev2.MatrixAPI,
|
||||
dm *signalpb.DataMessage,
|
||||
attMap AttachmentMap,
|
||||
|
|
@ -114,20 +108,8 @@ func (mc *MessageConverter) ToMatrix(
|
|||
// Don't allow any other parts in a sticker message
|
||||
return cm
|
||||
}
|
||||
if dm.PollVote != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertPollVoteToMatrix(ctx, sender, dm.PollVote))
|
||||
return cm
|
||||
}
|
||||
if dm.PollCreate != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertPollCreateToMatrix(dm.PollCreate))
|
||||
return cm
|
||||
}
|
||||
if dm.PollTerminate != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate))
|
||||
return cm
|
||||
}
|
||||
for i, att := range dm.GetAttachments() {
|
||||
if att.GetContentType() != "text/x-signal-plain" || att.GetSize() > matrixTextMaxLength {
|
||||
if att.GetContentType() != "text/x-signal-plain" {
|
||||
cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att, attMap))
|
||||
} else {
|
||||
longBody, err := mc.downloadSignalLongText(ctx, att, attMap)
|
||||
|
|
@ -178,12 +160,9 @@ func (mc *MessageConverter) ToMatrix(
|
|||
}
|
||||
}
|
||||
if dm.Quote != nil {
|
||||
authorACI, err := signalmeow.ParseStringOrBinaryUUID(dm.Quote.GetAuthorAci(), dm.Quote.GetAuthorAciBinary())
|
||||
authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Str("author_aci", dm.Quote.GetAuthorAci()).
|
||||
Hex("author_aci_binary", dm.Quote.GetAuthorAciBinary()).
|
||||
Msg("Failed to parse quote author ACI")
|
||||
zerolog.Ctx(ctx).Err(err).Str("author_aci", dm.Quote.GetAuthorAci()).Msg("Failed to parse quote author ACI")
|
||||
} else {
|
||||
cm.ReplyTo = &networkid.MessageOptionalPartID{
|
||||
MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()),
|
||||
|
|
@ -344,7 +323,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
|
|||
card.Add(vcard.FieldTelephone, &field)
|
||||
}
|
||||
if contact.GetAvatar().GetAvatar() != nil {
|
||||
avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap, nil)
|
||||
avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar")
|
||||
} else {
|
||||
|
|
@ -462,28 +441,31 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
|
|||
},
|
||||
}
|
||||
}
|
||||
// Signal stickers are 512x512, so tell Matrix clients to render them as 200x200 to match Signal
|
||||
// https://github.com/signalapp/Signal-Desktop/blob/v7.77.0-beta.1/ts/components/conversation/Message.dom.tsx#L135
|
||||
// Signal stickers are 512x512, so tell Matrix clients to render them as 256x256
|
||||
if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 {
|
||||
converted.Content.Info.Width = 200
|
||||
converted.Content.Info.Height = 200
|
||||
converted.Content.Info.Width = 256
|
||||
converted.Content.Info.Height = 256
|
||||
}
|
||||
converted.Content.Body = sticker.GetEmoji()
|
||||
if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) {
|
||||
converted.Content.Info.BridgedSticker = &event.BridgedSticker{
|
||||
Network: StickerSourceID,
|
||||
ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10),
|
||||
Emoji: sticker.GetEmoji(),
|
||||
PackURL: fmt.Sprintf(PackURLFormat, sticker.GetPackId(), sticker.GetPackKey()),
|
||||
}
|
||||
}
|
||||
converted.Type = event.EventSticker
|
||||
converted.Content.MsgType = ""
|
||||
if converted.Extra == nil {
|
||||
converted.Extra = map[string]any{}
|
||||
}
|
||||
// TODO fetch full pack metadata like the old bridge did?
|
||||
converted.Extra["fi.mau.signal.sticker"] = map[string]any{
|
||||
"id": sticker.GetStickerId(),
|
||||
"emoji": sticker.GetEmoji(),
|
||||
"pack": map[string]any{
|
||||
"id": sticker.GetPackId(),
|
||||
"key": sticker.GetPackKey(),
|
||||
},
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) {
|
||||
data, err := mc.downloadAttachment(ctx, att, attMap, nil)
|
||||
data, err := mc.downloadAttachment(ctx, att, attMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -509,9 +491,7 @@ func checkIfAttachmentExists(att *signalpb.AttachmentPointer, attMap AttachmentM
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) downloadAttachment(
|
||||
ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap, into *os.File,
|
||||
) ([]byte, error) {
|
||||
func (mc *MessageConverter) downloadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) ([]byte, error) {
|
||||
if err := checkIfAttachmentExists(att, attMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -522,19 +502,19 @@ func (mc *MessageConverter) downloadAttachment(
|
|||
plaintextHash = target.GetPlaintextHash()
|
||||
}
|
||||
}
|
||||
return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash, into)
|
||||
return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash)
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*bridgev2.ConvertedMessagePart, error) {
|
||||
fileName := att.GetFileName()
|
||||
content := &event.MessageEventContent{
|
||||
Body: att.GetFileName(),
|
||||
Info: &event.FileInfo{
|
||||
MimeType: att.GetContentType(),
|
||||
Width: int(att.GetWidth()),
|
||||
Height: int(att.GetHeight()),
|
||||
Size: int(att.GetSize()),
|
||||
},
|
||||
}
|
||||
mimeType := att.GetContentType()
|
||||
if err := checkIfAttachmentExists(att, attMap); err != nil {
|
||||
return nil, err
|
||||
} else if mc.DirectMedia {
|
||||
|
|
@ -561,7 +541,25 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
|
|||
}
|
||||
content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
|
||||
} else {
|
||||
err = mc.actuallyReuploadAttachment(ctx, content, att, attMap)
|
||||
data, err := mc.downloadAttachment(ctx, att, attMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mimeType == "" {
|
||||
mimeType = http.DetectContentType(data)
|
||||
}
|
||||
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mimeType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
|
||||
}
|
||||
fileName += ".ogg"
|
||||
mimeType = "audio/ogg"
|
||||
content.MSC3245Voice = &event.MSC3245Voice{}
|
||||
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
|
||||
//content.MSC1767Audio = &event.MSC1767Audio{}
|
||||
}
|
||||
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, fileName, mimeType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -570,21 +568,16 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
|
|||
content.Info.Blurhash = att.GetBlurHash()
|
||||
content.Info.AnoaBlurhash = att.GetBlurHash()
|
||||
}
|
||||
plainMime, _, _ := mime.ParseMediaType(content.Info.MimeType)
|
||||
// Supported mime types from https://github.com/signalapp/Signal-Desktop/blob/main/ts/util/GoogleChrome.std.ts
|
||||
switch plainMime {
|
||||
case "image/avif", "image/bmp", "image/gif", "image/jpeg", "image/webp", "image/x-xbitmap",
|
||||
"image/vnd.microsoft.icon", "image/ico", "image/icon", "image/x-icon", "image/png", "image/apng":
|
||||
switch strings.Split(mimeType, "/")[0] {
|
||||
case "image":
|
||||
content.MsgType = event.MsgImage
|
||||
case "video/mp4", "video/ogg", "video/webm":
|
||||
case "video":
|
||||
content.MsgType = event.MsgVideo
|
||||
default:
|
||||
if strings.HasPrefix(plainMime, "audio/") && !strings.HasSuffix(plainMime, "aiff") {
|
||||
case "audio":
|
||||
content.MsgType = event.MsgAudio
|
||||
} else {
|
||||
default:
|
||||
content.MsgType = event.MsgFile
|
||||
}
|
||||
}
|
||||
var extra map[string]any
|
||||
if att.GetFlags()&uint32(signalpb.AttachmentPointer_GIF) != 0 {
|
||||
content.Info.MauGIF = true
|
||||
|
|
@ -597,8 +590,10 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
|
|||
},
|
||||
}
|
||||
}
|
||||
content.Body = fileName
|
||||
content.Info.MimeType = mimeType
|
||||
if content.Body == "" {
|
||||
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(content.Info.MimeType)
|
||||
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
|
||||
}
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
|
|
@ -606,203 +601,3 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
|
|||
Extra: extra,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) actuallyReuploadAttachment(
|
||||
ctx context.Context,
|
||||
content *event.MessageEventContent,
|
||||
att *signalpb.AttachmentPointer,
|
||||
attMap AttachmentMap,
|
||||
) (err error) {
|
||||
convertVoice := att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported()
|
||||
requireFile := convertVoice
|
||||
content.URL, content.File, err = getIntent(ctx).UploadMediaStream(ctx, getPortal(ctx).MXID, int64(att.GetSize()), requireFile, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||
osFile, ok := file.(*os.File)
|
||||
inMemData, err := mc.downloadAttachment(ctx, att, attMap, osFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
if content.Info.MimeType == "" {
|
||||
content.Info.MimeType = http.DetectContentType(inMemData)
|
||||
}
|
||||
_, err = file.Write(inMemData)
|
||||
return &bridgev2.FileStreamResult{
|
||||
FileName: content.Body,
|
||||
MimeType: content.Info.MimeType,
|
||||
}, err
|
||||
}
|
||||
if content.Info.MimeType == "" {
|
||||
header := make([]byte, 512)
|
||||
_, err = osFile.ReadAt(header, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file header for MIME type detection: %w", err)
|
||||
} else {
|
||||
content.Info.MimeType = http.DetectContentType(header)
|
||||
}
|
||||
}
|
||||
var replFile string
|
||||
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
|
||||
replFile, err = ffmpeg.ConvertPath(ctx, osFile.Name(), ".ogg", []string{}, []string{"-c:a", "libopus"}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
|
||||
}
|
||||
if content.Body == "" {
|
||||
content.Body = "Voice message.ogg"
|
||||
} else {
|
||||
content.Body += ".ogg"
|
||||
}
|
||||
content.Info.MimeType = "audio/ogg"
|
||||
content.MSC3245Voice = &event.MSC3245Voice{}
|
||||
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
|
||||
//content.MSC1767Audio = &event.MSC1767Audio{}
|
||||
}
|
||||
return &bridgev2.FileStreamResult{
|
||||
ReplacementFile: replFile,
|
||||
FileName: content.Body,
|
||||
MimeType: content.Info.MimeType,
|
||||
}, nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPollCreateToMatrix(create *signalpb.DataMessage_PollCreate) *bridgev2.ConvertedMessagePart {
|
||||
evtType := event.EventMessage
|
||||
if mc.ExtEvPolls {
|
||||
evtType = event.EventUnstablePollStart
|
||||
}
|
||||
maxChoices := 1
|
||||
if create.GetAllowMultiple() {
|
||||
maxChoices = len(create.GetOptions())
|
||||
}
|
||||
msc3381Answers := make([]map[string]any, len(create.GetOptions()))
|
||||
optionsListText := make([]string, len(create.GetOptions()))
|
||||
optionsListHTML := make([]string, len(create.GetOptions()))
|
||||
for i, option := range create.GetOptions() {
|
||||
msc3381Answers[i] = map[string]any{
|
||||
"id": strconv.Itoa(i),
|
||||
"org.matrix.msc1767.text": option,
|
||||
}
|
||||
optionsListText[i] = fmt.Sprintf("%d. %s\n", i+1, option)
|
||||
optionsListHTML[i] = fmt.Sprintf("<li>%s</li>", event.TextToHTML(option))
|
||||
}
|
||||
body := fmt.Sprintf("%s\n\n%s\n\n(This message is a poll. Please open Signal to vote.)", create.GetQuestion(), strings.Join(optionsListText, "\n"))
|
||||
formattedBody := fmt.Sprintf("<p>%s</p><ol>%s</ol><p>(This message is a poll. Please open Signal to vote.)</p>", event.TextToHTML(create.GetQuestion()), strings.Join(optionsListHTML, ""))
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: evtType,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: body,
|
||||
Format: event.FormatHTML,
|
||||
FormattedBody: formattedBody,
|
||||
},
|
||||
Extra: map[string]any{
|
||||
"fi.mau.signal.poll": map[string]any{
|
||||
"question": create.GetQuestion(),
|
||||
"allow_multiple": create.GetAllowMultiple(),
|
||||
"options": create.GetOptions(),
|
||||
},
|
||||
"org.matrix.msc1767.message": []map[string]any{
|
||||
{"mimetype": "text/html", "body": formattedBody},
|
||||
{"mimetype": "text/plain", "body": body},
|
||||
},
|
||||
"org.matrix.msc3381.poll.start": map[string]any{
|
||||
"kind": "org.matrix.msc3381.poll.disclosed",
|
||||
"max_selections": maxChoices,
|
||||
"question": map[string]any{
|
||||
"org.matrix.msc1767.text": create.GetQuestion(),
|
||||
},
|
||||
"answers": msc3381Answers,
|
||||
},
|
||||
},
|
||||
DBMetadata: nil,
|
||||
DontBridge: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, senderACI uuid.UUID, terminate *signalpb.DataMessage_PollTerminate) *bridgev2.ConvertedMessagePart {
|
||||
pollMessageID := signalid.MakeMessageID(senderACI, terminate.GetTargetSentTimestamp())
|
||||
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll terminate target message")
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventUnstablePollEnd,
|
||||
Content: &event.MessageEventContent{},
|
||||
DontBridge: true,
|
||||
}
|
||||
}
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventUnstablePollEnd,
|
||||
Content: &event.MessageEventContent{
|
||||
RelatesTo: &event.RelatesTo{
|
||||
Type: event.RelReference,
|
||||
EventID: pollMessage.MXID,
|
||||
},
|
||||
},
|
||||
Extra: map[string]any{
|
||||
"org.matrix.msc3381.poll.end": map[string]any{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var invalidPollVote = &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventUnstablePollResponse,
|
||||
Content: &event.MessageEventContent{},
|
||||
DontBridge: true,
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPollVoteToMatrix(ctx context.Context, senderACI uuid.UUID, vote *signalpb.DataMessage_PollVote) *bridgev2.ConvertedMessagePart {
|
||||
if len(vote.GetTargetAuthorAciBinary()) != 16 {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("author_aci_b64", base64.StdEncoding.EncodeToString(vote.GetTargetAuthorAciBinary())).
|
||||
Msg("Invalid author ACI in poll vote")
|
||||
return invalidPollVote
|
||||
}
|
||||
pollMessageID := signalid.MakeMessageID(uuid.UUID(vote.GetTargetAuthorAciBinary()), vote.GetTargetSentTimestamp())
|
||||
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll vote target message")
|
||||
return invalidPollVote
|
||||
} else if pollMessage == nil {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Poll vote target message not found")
|
||||
return invalidPollVote
|
||||
}
|
||||
meta := pollMessage.Metadata.(*signalid.MessageMetadata)
|
||||
if prevCount, ok := meta.VoteCount[senderACI.String()]; ok && vote.GetVoteCount() <= prevCount {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("sender_aci", senderACI).
|
||||
Uint32("vote_count", vote.GetVoteCount()).
|
||||
Uint32("previous_vote_count", prevCount).
|
||||
Msg("Ignoring poll vote with lower vote count")
|
||||
return invalidPollVote
|
||||
}
|
||||
if meta.VoteCount == nil {
|
||||
meta.VoteCount = make(map[string]uint32)
|
||||
}
|
||||
meta.VoteCount[senderACI.String()] = vote.GetVoteCount()
|
||||
err = mc.Bridge.DB.Message.Update(ctx, pollMessage)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to update poll message with new vote count")
|
||||
}
|
||||
mxOptionIDs := meta.MatrixPollOptionIDs
|
||||
optionIDs := make([]string, len(vote.GetOptionIndexes()))
|
||||
for i, optionIndex := range vote.GetOptionIndexes() {
|
||||
if int(optionIndex) < len(mxOptionIDs) {
|
||||
optionIDs[i] = mxOptionIDs[optionIndex]
|
||||
} else {
|
||||
optionIDs[i] = strconv.Itoa(int(optionIndex))
|
||||
}
|
||||
}
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventUnstablePollResponse,
|
||||
Content: &event.MessageEventContent{
|
||||
RelatesTo: &event.RelatesTo{
|
||||
Type: event.RelReference,
|
||||
EventID: pollMessage.MXID,
|
||||
},
|
||||
},
|
||||
Extra: map[string]any{
|
||||
"org.matrix.msc3381.poll.response": map[string]any{
|
||||
"answers": optionIDs,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -111,13 +111,22 @@ func TestParse_HTML(t *testing.T) {
|
|||
{
|
||||
name: "List",
|
||||
in: "<ul><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ul>",
|
||||
out: "* woof\n* meow\n* ```\n hmm\n meow\n ```\n* > meow\n > \n > # meow",
|
||||
out: "* woof\n* meow\n* hmm\n meow\n* > meow\n > \n > # meow",
|
||||
ent: signalfmt.BodyRangeList{{
|
||||
Start: 9,
|
||||
Length: 4,
|
||||
Value: signalfmt.StyleBold,
|
||||
}, {
|
||||
Start: 57,
|
||||
Start: 16,
|
||||
Length: 3,
|
||||
Value: signalfmt.StyleMonospace,
|
||||
}, {
|
||||
// FIXME optimally this would be a single range with the previous one so the indent is also monospace
|
||||
Start: 22,
|
||||
Length: 4,
|
||||
Value: signalfmt.StyleMonospace,
|
||||
}, {
|
||||
Start: 45,
|
||||
Length: 6,
|
||||
Value: signalfmt.StyleBold,
|
||||
}},
|
||||
|
|
@ -125,13 +134,21 @@ func TestParse_HTML(t *testing.T) {
|
|||
{
|
||||
name: "OrderedList",
|
||||
in: "<ol start=9><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ol>",
|
||||
out: "9. woof\n10. meow\n11. ```\n hmm\n meow\n ```\n12. > meow\n > \n > # meow",
|
||||
out: "9. woof\n10. meow\n11. hmm\n meow\n12. > meow\n > \n > # meow",
|
||||
ent: signalfmt.BodyRangeList{{
|
||||
Start: 13,
|
||||
Length: 4,
|
||||
Value: signalfmt.StyleBold,
|
||||
}, {
|
||||
Start: 75,
|
||||
Start: 22,
|
||||
Length: 3,
|
||||
Value: signalfmt.StyleMonospace,
|
||||
}, {
|
||||
Start: 30,
|
||||
Length: 4,
|
||||
Value: signalfmt.StyleMonospace,
|
||||
}, {
|
||||
Start: 59,
|
||||
Length: 6,
|
||||
Value: signalfmt.StyleBold,
|
||||
}},
|
||||
|
|
|
|||
|
|
@ -404,17 +404,17 @@ func (parser *HTMLParser) tagToString(node *html.Node, ctx Context) *EntityStrin
|
|||
return NewEntityString("---")
|
||||
case "pre":
|
||||
var preStr *EntityString
|
||||
var language string
|
||||
//var language string
|
||||
if node.FirstChild != nil && node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" {
|
||||
class := parser.getAttribute(node.FirstChild, "class")
|
||||
if strings.HasPrefix(class, "language-") {
|
||||
language = class[len("language-"):]
|
||||
}
|
||||
//class := parser.getAttribute(node.FirstChild, "class")
|
||||
//if strings.HasPrefix(class, "language-") {
|
||||
// language = class[len("language-"):]
|
||||
//}
|
||||
preStr = parser.nodeToString(node.FirstChild.FirstChild, ctx.WithWhitespace())
|
||||
} else {
|
||||
preStr = parser.nodeToString(node.FirstChild, ctx.WithWhitespace())
|
||||
}
|
||||
return NewEntityString(fmt.Sprintf("```%s\n", language)).Append(preStr).AppendString("\n```")
|
||||
return preStr.Format(signalfmt.StyleMonospace)
|
||||
default:
|
||||
return parser.nodeToTagAwareString(node.FirstChild, ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,13 +29,10 @@ type PortalMetadata struct {
|
|||
|
||||
type MessageMetadata struct {
|
||||
ContainsAttachments bool `json:"contains_attachments,omitempty"`
|
||||
MatrixPollOptionIDs []string `json:"matrix_poll_option_ids,omitempty"`
|
||||
VoteCount map[string]uint32 `json:"vote_count,omitempty"`
|
||||
}
|
||||
|
||||
type UserLoginMetadata struct {
|
||||
ChatsSynced bool `json:"chats_synced,omitempty"`
|
||||
LastContactSync jsontime.UnixMilli `json:"last_contact_sync,omitempty"`
|
||||
}
|
||||
|
||||
type GhostMetadata struct {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -31,11 +31,8 @@ import (
|
|||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/fallocate"
|
||||
"go.mau.fi/util/pkcs7"
|
||||
"go.mau.fi/util/random"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
|
|
@ -62,54 +59,26 @@ var ErrInvalidMACForAttachment = errors.New("invalid MAC for attachment")
|
|||
var ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment")
|
||||
var ErrAttachmentNotFound = errors.New("attachment not found on server")
|
||||
|
||||
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte, into *os.File) ([]byte, error) {
|
||||
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte) ([]byte, error) {
|
||||
digest := a.GetDigest()
|
||||
plaintextDigest := false
|
||||
if digest == nil && plaintextHash != nil {
|
||||
digest = plaintextHash
|
||||
plaintextDigest = true
|
||||
}
|
||||
return DownloadAttachment(
|
||||
ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize(), into,
|
||||
)
|
||||
return DownloadAttachment(ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize())
|
||||
}
|
||||
|
||||
func DownloadAttachment(
|
||||
ctx context.Context,
|
||||
cdnID uint64,
|
||||
cdnKey string,
|
||||
cdnNumber uint32,
|
||||
key, digest []byte,
|
||||
plaintextDigest bool,
|
||||
size uint32,
|
||||
into *os.File,
|
||||
) ([]byte, error) {
|
||||
resp, err := web.GetAttachment(ctx, getAttachmentPath(cdnID, cdnKey), cdnNumber)
|
||||
func DownloadAttachment(ctx context.Context, cdnID uint64, cdnKey string, cdnNumber uint32, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
|
||||
path := getAttachmentPath(cdnID, cdnKey)
|
||||
resp, err := web.GetAttachment(ctx, path, cdnNumber, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
bodyReader := resp.Body
|
||||
defer bodyReader.Close()
|
||||
|
||||
var body []byte
|
||||
var downloadedSize int64
|
||||
if resp.StatusCode > 400 {
|
||||
body, err = io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
} else if into == nil {
|
||||
if resp.ContentLength > 0 {
|
||||
body = make([]byte, resp.ContentLength)
|
||||
_, err = io.ReadFull(resp.Body, body)
|
||||
} else {
|
||||
body, err = io.ReadAll(http.MaxBytesReader(nil, resp.Body, max(int64(size), 32*1024)*2))
|
||||
}
|
||||
} else {
|
||||
err = fallocate.Fallocate(into, int(resp.ContentLength))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pre-allocate file for attachment: %w", err)
|
||||
}
|
||||
downloadedSize, err = io.Copy(into, resp.Body)
|
||||
}
|
||||
body, err := io.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -125,27 +94,12 @@ func DownloadAttachment(
|
|||
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if into != nil {
|
||||
if _, err = into.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("failed to seek attachment file after downloading: %w", err)
|
||||
}
|
||||
return nil, decryptAttachmentFile(into, downloadedSize, key, digest, plaintextDigest, size)
|
||||
}
|
||||
return decryptAttachment(body, key, digest, plaintextDigest, size)
|
||||
}
|
||||
|
||||
const MACLength = 32
|
||||
const IVLength = 16
|
||||
|
||||
func macAndAESDecrypt(body, key []byte) ([]byte, error) {
|
||||
l := len(body) - MACLength
|
||||
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
|
||||
return nil, ErrInvalidMACForAttachment
|
||||
}
|
||||
|
||||
return aesDecrypt(key[:MACLength], body[:l])
|
||||
}
|
||||
|
||||
func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
|
||||
if !plaintextDigest {
|
||||
hash := sha256.Sum256(body)
|
||||
|
|
@ -153,7 +107,12 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
|
|||
return nil, ErrInvalidDigestForAttachment
|
||||
}
|
||||
}
|
||||
decrypted, err := macAndAESDecrypt(body, key)
|
||||
l := len(body) - MACLength
|
||||
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
|
||||
return nil, ErrInvalidMACForAttachment
|
||||
}
|
||||
|
||||
decrypted, err := aesDecrypt(key[:MACLength], body[:l])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -170,59 +129,6 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
|
|||
return decrypted, nil
|
||||
}
|
||||
|
||||
func decryptAttachmentFile(file *os.File, downloadedSize int64, key, digest []byte, plaintextDigest bool, size uint32) error {
|
||||
if !plaintextDigest {
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return fmt.Errorf("failed to hash attachment file: %w", err)
|
||||
} else if !hmac.Equal(hasher.Sum(nil), digest) {
|
||||
return ErrInvalidDigestForAttachment
|
||||
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("failed to seek attachment file after hashing: %w", err)
|
||||
}
|
||||
}
|
||||
mac := make([]byte, MACLength)
|
||||
n, err := file.ReadAt(mac, downloadedSize-MACLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read MAC from attachment file: %w", err)
|
||||
} else if n != MACLength {
|
||||
return fmt.Errorf("unexpected MAC length read from attachment file: %d", n)
|
||||
}
|
||||
hasher := hmac.New(sha256.New, key[MACLength:])
|
||||
_, err = io.CopyN(hasher, file, downloadedSize-MACLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash attachment file for MAC verification: %w", err)
|
||||
} else if !hmac.Equal(hasher.Sum(nil), mac) {
|
||||
return ErrInvalidMACForAttachment
|
||||
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("failed to seek attachment file after verifying mac: %w", err)
|
||||
}
|
||||
|
||||
decryptedSize, err := aesDecryptFile(key[:MACLength], file, downloadedSize-MACLength)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if decryptedSize < int64(size) {
|
||||
return fmt.Errorf("decrypted attachment length %d < expected %d", decryptedSize, size)
|
||||
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("failed to seek attachment file after decrypting: %w", err)
|
||||
}
|
||||
err = file.Truncate(int64(size))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to truncate attachment file to expected size: %w", err)
|
||||
}
|
||||
if plaintextDigest {
|
||||
hasher = sha256.New()
|
||||
if _, err = io.Copy(hasher, file); err != nil {
|
||||
return fmt.Errorf("failed to hash decrypted attachment file: %w", err)
|
||||
} else if !hmac.Equal(hasher.Sum(nil), digest) {
|
||||
return fmt.Errorf("%w (plaintext hash)", ErrInvalidDigestForAttachment)
|
||||
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("failed to seek attachment file after hashing plaintext: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type attachmentV4UploadAttributes struct {
|
||||
Cdn uint32 `json:"cdn"`
|
||||
Key string `json:"key"`
|
||||
|
|
@ -245,14 +151,6 @@ func extend(data []byte, paddedLen int) []byte {
|
|||
}
|
||||
}
|
||||
|
||||
func macAndAESEncrypt(keys, plaintext []byte) ([]byte, error) {
|
||||
encrypted, err := aesEncrypt(keys[:32], plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appendMAC(keys[32:], encrypted), nil
|
||||
}
|
||||
|
||||
func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
|
||||
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
|
||||
keys := random.Bytes(64) // combined AES and MAC keys
|
||||
|
|
@ -268,20 +166,23 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
|
|||
}
|
||||
body = extend(body, paddedLen)
|
||||
|
||||
encryptedWithMAC, err := macAndAESEncrypt(keys, body)
|
||||
encrypted, err := aesEncrypt(keys[:32], body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedWithMAC := appendMAC(keys[32:], encrypted)
|
||||
|
||||
// Get upload attributes from Signal server
|
||||
attributesPath := "/v4/attachments/form/upload"
|
||||
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil)
|
||||
username, password := cli.Store.BasicAuthCreds()
|
||||
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, attributesPath, opts)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to request upload attributes")
|
||||
return nil, fmt.Errorf("failed to request upload attributes: %w", err)
|
||||
}
|
||||
var uploadAttributes attachmentV4UploadAttributes
|
||||
err = web.DecodeWSResponseBody(ctx, &uploadAttributes, resp)
|
||||
err = web.DecodeHTTPResponseBody(ctx, &uploadAttributes, resp)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to decode upload attributes")
|
||||
return nil, fmt.Errorf("failed to decode upload attributes: %w", err)
|
||||
|
|
@ -291,7 +192,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
|
|||
err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC)
|
||||
} else {
|
||||
log.Trace().Msg("Using legacy upload")
|
||||
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC)
|
||||
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC, username, password)
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to upload attachment")
|
||||
|
|
@ -317,17 +218,17 @@ func (cli *Client) uploadAttachmentLegacy(
|
|||
ctx context.Context,
|
||||
uploadAttributes attachmentV4UploadAttributes,
|
||||
encryptedWithMAC []byte,
|
||||
username string,
|
||||
password string,
|
||||
) error {
|
||||
username, password := cli.Store.BasicAuthCreds()
|
||||
// Allocate attachment on CDN
|
||||
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
|
||||
OverrideURL: uploadAttributes.SignedUploadLocation,
|
||||
ContentType: web.ContentTypeOctetStream,
|
||||
Headers: uploadAttributes.Headers,
|
||||
Username: &username,
|
||||
Password: &password,
|
||||
})
|
||||
web.CloseBody(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send allocate request: %w", err)
|
||||
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
|
|
@ -335,14 +236,13 @@ func (cli *Client) uploadAttachmentLegacy(
|
|||
}
|
||||
|
||||
// Upload attachment to CDN
|
||||
resp, err = web.SendHTTPRequest(ctx, "", http.MethodPut, "", &web.HTTPReqOpt{
|
||||
resp, err = web.SendHTTPRequest(ctx, http.MethodPut, "", &web.HTTPReqOpt{
|
||||
OverrideURL: resp.Header.Get("Location"),
|
||||
Body: encryptedWithMAC,
|
||||
ContentType: web.ContentTypeOctetStream,
|
||||
Username: &username,
|
||||
Password: &password,
|
||||
})
|
||||
web.CloseBody(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send upload request: %w", err)
|
||||
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
|
|
@ -360,13 +260,12 @@ func (cli *Client) uploadAttachmentTUS(
|
|||
uploadAttributes.Headers["Upload-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC))
|
||||
uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key))
|
||||
|
||||
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
|
||||
OverrideURL: uploadAttributes.SignedUploadLocation,
|
||||
Body: encryptedWithMAC,
|
||||
ContentType: web.ContentTypeOffsetOctetStream,
|
||||
Headers: uploadAttributes.Headers,
|
||||
})
|
||||
web.CloseBody(resp)
|
||||
// TODO actually support resuming on error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send upload request: %w", err)
|
||||
|
|
@ -381,17 +280,12 @@ func (cli *Client) uploadAttachmentTUS(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier, groupMasterKey types.SerializedGroupMasterKey) (string, error) {
|
||||
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier) (string, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
if groupMasterKey == "" {
|
||||
var err error
|
||||
groupMasterKey, err = cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
|
||||
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Could not get master key from group id")
|
||||
return "", err
|
||||
} else if groupMasterKey == "" {
|
||||
return "", fmt.Errorf("no master key found for group %s", gid)
|
||||
}
|
||||
}
|
||||
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
|
||||
if err != nil {
|
||||
|
|
@ -411,15 +305,14 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
|
|||
}
|
||||
|
||||
// Get upload form from Signal server
|
||||
formPath := "/v2/groups/avatar/form"
|
||||
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf}
|
||||
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, formPath, opts)
|
||||
formPath := "/v1/groups/avatar/form"
|
||||
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf, Host: web.StorageHostname}
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, formPath, opts)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error sending request fetching avatar upload form")
|
||||
return "", err
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
web.CloseBody(resp)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error decoding response body fetching upload attributes")
|
||||
return "", err
|
||||
|
|
@ -445,11 +338,11 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
|
|||
w.Close()
|
||||
|
||||
// Upload avatar to CDN
|
||||
resp, err = web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodPost, "", &web.HTTPReqOpt{
|
||||
resp, err = web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
|
||||
Body: requestBody.Bytes(),
|
||||
ContentType: web.ContentType(w.FormDataContentType()),
|
||||
Host: web.CDN1Hostname,
|
||||
})
|
||||
web.CloseBody(resp)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error sending request uploading attachment")
|
||||
return "", err
|
||||
|
|
@ -478,56 +371,14 @@ func aesDecrypt(key, ciphertext []byte) ([]byte, error) {
|
|||
return nil, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", len(ciphertext)%aes.BlockSize)
|
||||
}
|
||||
|
||||
iv := ciphertext[:IVLength]
|
||||
ciphertext = ciphertext[IVLength:]
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext, ciphertext)
|
||||
return pkcs7.Unpad(ciphertext)
|
||||
}
|
||||
|
||||
func aesDecryptFile(key []byte, file *os.File, downloadedSize int64) (int64, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fileReader := io.LimitReader(file, downloadedSize)
|
||||
|
||||
if downloadedSize%aes.BlockSize != 0 {
|
||||
return 0, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", downloadedSize%aes.BlockSize)
|
||||
}
|
||||
|
||||
iv := make([]byte, IVLength)
|
||||
n, err := fileReader.Read(iv)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read IV from attachment file: %w", err)
|
||||
} else if n != IVLength {
|
||||
return 0, fmt.Errorf("unexpected IV length read from attachment file: %d", n)
|
||||
}
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
buf := make([]byte, 4096)
|
||||
var offset int64
|
||||
var pad byte
|
||||
for {
|
||||
n, err = fileReader.Read(buf)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, fmt.Errorf("failed to read from attachment file: %w", err)
|
||||
}
|
||||
if n > 0 {
|
||||
mode.CryptBlocks(buf[:n], buf[:n])
|
||||
if _, err = file.WriteAt(buf[:n], offset); err != nil {
|
||||
return 0, fmt.Errorf("failed to write decrypted data to attachment file: %w", err)
|
||||
}
|
||||
offset += int64(n)
|
||||
pad = buf[n-1]
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
}
|
||||
pad := ciphertext[len(ciphertext)-1]
|
||||
if pad > aes.BlockSize {
|
||||
return 0, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
|
||||
return nil, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
|
||||
}
|
||||
return downloadedSize - int64(pad), nil
|
||||
return ciphertext[aes.BlockSize : len(ciphertext)-int(pad)], nil
|
||||
}
|
||||
|
||||
func appendMAC(key, body []byte) []byte {
|
||||
|
|
@ -542,11 +393,14 @@ func aesEncrypt(key, plaintext []byte) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
|
||||
pad := aes.BlockSize - len(plaintext)%aes.BlockSize
|
||||
plaintext = append(plaintext, bytes.Repeat([]byte{byte(pad)}, pad)...)
|
||||
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
iv := random.Bytes(16)
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(plaintext, plaintext)
|
||||
mode.CryptBlocks(ciphertext, plaintext)
|
||||
|
||||
return append(iv, plaintext...), nil
|
||||
return append(iv, ciphertext...), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,21 +18,16 @@ package signalmeow
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
||||
)
|
||||
|
||||
|
|
@ -40,14 +35,12 @@ type Client struct {
|
|||
Store *store.Device
|
||||
Log zerolog.Logger
|
||||
|
||||
senderCertificateWithE164 *libsignalgo.SenderCertificate
|
||||
senderCertificateNoE164 *libsignalgo.SenderCertificate
|
||||
senderCertificateCache sync.Mutex
|
||||
|
||||
sendCache *exsync.RingBuffer[sendCacheKey, *signalpb.Content]
|
||||
|
||||
SenderCertificateWithE164 *libsignalgo.SenderCertificate
|
||||
SenderCertificateNoE164 *libsignalgo.SenderCertificate
|
||||
GroupCredentials *GroupCredentials
|
||||
GroupCache *GroupCache
|
||||
ProfileCache *ProfileCache
|
||||
GroupCallCache *map[string]bool
|
||||
LastContactRequestTime time.Time
|
||||
SyncContactsOnConnect bool
|
||||
|
||||
|
|
@ -71,26 +64,6 @@ type Client struct {
|
|||
writeCallbackCounter chan time.Time
|
||||
}
|
||||
|
||||
// InMemorySendCacheSize specifies how large the cache for sent messages is, which is used to respond to retry receipts.
|
||||
// The cache is large because every group member will be listed separately.
|
||||
// 2k entries should hold at least 2 messages in max size groups.
|
||||
var InMemorySendCacheSize = 2048
|
||||
|
||||
func NewClient(device *store.Device, log zerolog.Logger, evtHandler func(events.SignalEvent) bool) *Client {
|
||||
return &Client{
|
||||
Store: device,
|
||||
Log: log,
|
||||
EventHandler: evtHandler,
|
||||
GroupCache: NewGroupCache(device.ACIServiceID()),
|
||||
ProfileCache: &ProfileCache{
|
||||
profiles: make(map[string]*types.Profile),
|
||||
errors: make(map[string]*error),
|
||||
lastFetched: make(map[string]time.Time),
|
||||
},
|
||||
sendCache: exsync.NewRingBuffer[sendCacheKey, *signalpb.Content](InMemorySendCacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *Client) handleEvent(evt events.SignalEvent) bool {
|
||||
return cli.EventHandler(evt)
|
||||
}
|
||||
|
|
@ -137,11 +110,3 @@ func (cli *Client) connectUnauthedWS(ctx context.Context) (chan web.SignalWebsoc
|
|||
func (cli *Client) IsLoggedIn() bool {
|
||||
return cli.Store != nil && cli.Store.IsDeviceLoggedIn()
|
||||
}
|
||||
|
||||
func (cli *Client) GetRemoteConfig(ctx context.Context) (json.RawMessage, error) {
|
||||
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, "/v2/config", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, web.DecodeWSResponseBody(ctx, nil, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,12 @@ import (
|
|||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/cds2pb"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
||||
)
|
||||
|
||||
// ProdContactDiscoveryMrenclave should always match ENCLAVE_ID_CDSI_PROD from libsignal
|
||||
// https://github.com/signalapp/libsignal/blob/main/rust/attest/src/constants.rs#L69
|
||||
const ProdContactDiscoveryMrenclave = "15637fa1e54fe655176d3df1a9f94b87c01ed377acaa570682dc5d72c95ef07b"
|
||||
const ProdContactDiscoveryServer = "cdsi.signal.org"
|
||||
const ProdContactDiscoveryMrenclave = "ee9503070127120074612b6688e593b67e486b1541449f54d71e387484eb40a3"
|
||||
const ContactDiscoveryAuthTTL = 23 * time.Hour
|
||||
|
||||
const rateLimitCloseCode = websocket.StatusCode(4008)
|
||||
|
|
@ -83,7 +81,7 @@ func (cli *Client) LookupPhone(ctx context.Context, e164s ...uint64) (ContactDis
|
|||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
resp, token, err := cli.doContactDiscovery(ctx, &cds2pb.ClientRequest{
|
||||
resp, token, err := cli.doContactDiscovery(ctx, &signalpb.CDSClientRequest{
|
||||
// TODO figure out if tokens are useful
|
||||
// (it's meant for old_e164s)
|
||||
//Token: cli.cdToken,
|
||||
|
|
@ -95,7 +93,7 @@ func (cli *Client) LookupPhone(ctx context.Context, e164s ...uint64) (ContactDis
|
|||
return resp, err
|
||||
}
|
||||
|
||||
func (cli *Client) doContactDiscovery(ctx context.Context, req *cds2pb.ClientRequest) (ContactDiscoveryResponse, []byte, error) {
|
||||
func (cli *Client) doContactDiscovery(ctx context.Context, req *signalpb.CDSClientRequest) (ContactDiscoveryResponse, []byte, error) {
|
||||
creds, err := cli.getContactDiscoveryCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch contact discovery auth: %w", err)
|
||||
|
|
@ -187,7 +185,7 @@ func (cdc *ContactDiscoveryClient) Handshake(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cdc *ContactDiscoveryClient) SendRequest(ctx context.Context, req *cds2pb.ClientRequest) error {
|
||||
func (cdc *ContactDiscoveryClient) SendRequest(ctx context.Context, req *signalpb.CDSClientRequest) error {
|
||||
plaintext, err := proto.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
|
|
@ -224,15 +222,15 @@ func (cdc *ContactDiscoveryClient) handleResponse(ctx context.Context, msg []byt
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt message: %w", err)
|
||||
}
|
||||
var cdsClientResp cds2pb.ClientResponse
|
||||
var cdsClientResp signalpb.CDSClientResponse
|
||||
err = proto.Unmarshal(decrypted, &cdsClientResp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal message: %w", err)
|
||||
}
|
||||
if cdsClientResp.Token != nil {
|
||||
cdc.Token = cdsClientResp.Token
|
||||
err = cdc.SendRequest(ctx, &cds2pb.ClientRequest{
|
||||
TokenAck: true,
|
||||
err = cdc.SendRequest(ctx, &signalpb.CDSClientRequest{
|
||||
TokenAck: proto.Bool(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send token ack request: %w", err)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ func (*Call) isSignalEvent() {}
|
|||
func (*ContactList) isSignalEvent() {}
|
||||
func (*ACIFound) isSignalEvent() {}
|
||||
func (*DeleteForMe) isSignalEvent() {}
|
||||
func (*MessageRequestResponse) isSignalEvent() {}
|
||||
func (*QueueEmpty) isSignalEvent() {}
|
||||
func (*LoggedOut) isSignalEvent() {}
|
||||
|
||||
|
|
@ -90,14 +89,6 @@ type DeleteForMe struct {
|
|||
*signalpb.SyncMessage_DeleteForMe
|
||||
}
|
||||
|
||||
type MessageRequestResponse struct {
|
||||
Timestamp uint64
|
||||
ThreadACI uuid.UUID
|
||||
GroupID *libsignalgo.GroupIdentifier
|
||||
Type signalpb.SyncMessage_MessageRequestResponse_Type
|
||||
Raw *signalpb.SyncMessage_MessageRequestResponse
|
||||
}
|
||||
|
||||
type QueueEmpty struct{}
|
||||
|
||||
type LoggedOut struct{ Error error }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/util/random"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
|
@ -91,12 +90,6 @@ type Group struct {
|
|||
//PublicKey *libsignalgo.PublicKey
|
||||
}
|
||||
|
||||
func (group *Group) getMemberServiceIDs() []libsignalgo.ServiceID {
|
||||
return exslices.CastFunc(group.Members, func(from *GroupMember) libsignalgo.ServiceID {
|
||||
return libsignalgo.NewACIServiceID(from.ACI)
|
||||
})
|
||||
}
|
||||
|
||||
func (group *Group) GetInviteLink() (string, error) {
|
||||
if group.InviteLinkPassword == nil {
|
||||
return "", fmt.Errorf("no invite link password set")
|
||||
|
|
@ -106,8 +99,8 @@ func (group *Group) GetInviteLink() (string, error) {
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't decode invite link password")
|
||||
}
|
||||
inviteLinkContents := signalpb.GroupInviteLink_ContentsV1{
|
||||
ContentsV1: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
|
||||
inviteLinkContents := signalpb.GroupInviteLink_V1Contents{
|
||||
V1Contents: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
|
||||
GroupMasterKey: masterKeyBytes[:],
|
||||
InviteLinkPassword: inviteLinkPasswordBytes,
|
||||
},
|
||||
|
|
@ -121,15 +114,6 @@ func (group *Group) GetInviteLink() (string, error) {
|
|||
return "https://signal.group/#" + inviteLinkPath, nil
|
||||
}
|
||||
|
||||
func (group *Group) findMemberOrEmpty(aci uuid.UUID) *GroupMember {
|
||||
for _, member := range group.Members {
|
||||
if member.ACI == aci {
|
||||
return member
|
||||
}
|
||||
}
|
||||
return &GroupMember{}
|
||||
}
|
||||
|
||||
type GroupAccessControl struct {
|
||||
Members AccessControl
|
||||
AddFromInviteLink AccessControl
|
||||
|
|
@ -226,7 +210,8 @@ func (groupChange *GroupChange) isEmpty() bool {
|
|||
len(groupChange.PromoteRequestingMembers) == 0 &&
|
||||
groupChange.ModifyDescription == nil &&
|
||||
groupChange.ModifyAnnouncementsOnly == nil &&
|
||||
len(groupChange.AddBannedMembers) == 0
|
||||
len(groupChange.AddBannedMembers) == 0 &&
|
||||
len(groupChange.DeleteMembers) == 0
|
||||
}
|
||||
|
||||
func (groupChange *GroupChange) resolveConflict(group *Group) {
|
||||
|
|
@ -329,7 +314,8 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
|
|||
Logger()
|
||||
sevenDaysOut := today.Add(7 * 24 * time.Hour)
|
||||
path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix())
|
||||
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, path, nil, nil)
|
||||
authRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
|
||||
resp, err := cli.AuthedWS.SendRequest(ctx, authRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SendRequest error: %w", err)
|
||||
}
|
||||
|
|
@ -343,22 +329,51 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
|
|||
log.Err(err).Msg("json.Unmarshal error")
|
||||
return nil, err
|
||||
}
|
||||
// make sure pni matches device pni
|
||||
if creds.PNI != cli.Store.PNI {
|
||||
return nil, fmt.Errorf("mismatching PNI in group credentials: %s != %s", creds.PNI, cli.Store.PNI)
|
||||
err := fmt.Errorf("creds.PNI != d.PNI")
|
||||
log.Err(err).Msg("creds.PNI != d.PNI")
|
||||
return nil, err
|
||||
}
|
||||
return &creds, nil
|
||||
}
|
||||
|
||||
func (cli *Client) getCachedAuthorizationForToday(today time.Time) *GroupCredential {
|
||||
if cli.GroupCredentials == nil {
|
||||
// No cached credentials
|
||||
return nil
|
||||
}
|
||||
allCreds := cli.GroupCredentials
|
||||
// Get the credential for today
|
||||
for _, cred := range allCreds.Credentials {
|
||||
if cred.RedemptionTime == today.Unix() {
|
||||
return &cred
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) {
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("action", "get authorization for today").
|
||||
Logger()
|
||||
// Timestamps for the start of today, and 7 days later
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
todayCred, err := cli.GroupCache.GetCredentials(ctx, cli.fetchNewGroupCreds)
|
||||
todayCred := cli.getCachedAuthorizationForToday(today)
|
||||
if todayCred == nil {
|
||||
creds, err := cli.fetchNewGroupCreds(ctx, today)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group credentials: %w", err)
|
||||
return nil, fmt.Errorf("fetchNewGroupCreds error: %w", err)
|
||||
}
|
||||
cli.GroupCredentials = creds
|
||||
todayCred = cli.getCachedAuthorizationForToday(today)
|
||||
}
|
||||
if todayCred == nil {
|
||||
return nil, fmt.Errorf("couldn't get credential for today")
|
||||
}
|
||||
|
||||
//TODO: cache cred after unmarshalling
|
||||
redemptionTime := uint64(todayCred.RedemptionTime)
|
||||
credential := todayCred.Credential
|
||||
authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(credential)
|
||||
|
|
@ -470,7 +485,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
|
|||
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description)
|
||||
if err == nil {
|
||||
// treat a failure in obtaining the description as non-fatal
|
||||
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescriptionText())
|
||||
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescription())
|
||||
}
|
||||
|
||||
if encryptedGroup.DisappearingMessagesTimer != nil && len(encryptedGroup.DisappearingMessagesTimer) > 0 {
|
||||
|
|
@ -482,8 +497,8 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
|
|||
}
|
||||
|
||||
// These aren't encrypted
|
||||
decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl
|
||||
decryptedGroup.Revision = encryptedGroup.Version
|
||||
decryptedGroup.AvatarPath = encryptedGroup.Avatar
|
||||
decryptedGroup.Revision = encryptedGroup.Revision
|
||||
|
||||
// Decrypt members
|
||||
for _, member := range encryptedGroup.Members {
|
||||
|
|
@ -497,7 +512,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
|
|||
decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember)
|
||||
}
|
||||
|
||||
for _, pendingMember := range encryptedGroup.MembersPendingProfileKey {
|
||||
for _, pendingMember := range encryptedGroup.PendingMembers {
|
||||
if pendingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -509,7 +524,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
|
|||
decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember)
|
||||
}
|
||||
|
||||
for _, requestingMember := range encryptedGroup.MembersPendingAdminApproval {
|
||||
for _, requestingMember := range encryptedGroup.RequestingMembers {
|
||||
if requestingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -520,7 +535,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
|
|||
decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember)
|
||||
}
|
||||
|
||||
for _, bannedMember := range encryptedGroup.MembersBanned {
|
||||
for _, bannedMember := range encryptedGroup.BannedMembers {
|
||||
if bannedMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -619,7 +634,7 @@ func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier
|
|||
return nil, fmt.Errorf("failed to get group master key: %w", err)
|
||||
}
|
||||
if groupMasterKey == "" {
|
||||
return nil, fmt.Errorf("%w for %s", ErrGroupMasterKeyNotFound, gid)
|
||||
return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
|
||||
}
|
||||
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
|
||||
}
|
||||
|
|
@ -634,14 +649,14 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
|
|||
Username: &groupAuth.Username,
|
||||
Password: &groupAuth.Password,
|
||||
ContentType: web.ContentTypeProtobuf,
|
||||
Host: web.StorageHostname,
|
||||
}
|
||||
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, "/v2/groups", opts)
|
||||
defer web.CloseBody(response)
|
||||
response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v2/groups", opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected response status: %d", response.StatusCode)
|
||||
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
|
||||
}
|
||||
return cli.parseGroupResponse(ctx, response, groupMasterKey)
|
||||
}
|
||||
|
|
@ -661,10 +676,6 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt group: %w", err)
|
||||
}
|
||||
err = cli.GroupCache.Put(group, groupResponse.GroupSendEndorsementsResponse)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to cache group response")
|
||||
}
|
||||
|
||||
// Store the profile keys in case they're new
|
||||
for _, member := range group.Members {
|
||||
|
|
@ -685,11 +696,11 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
|
|||
func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) {
|
||||
username, password := cli.Store.BasicAuthCreds()
|
||||
opts := &web.HTTPReqOpt{
|
||||
Host: web.CDN1Hostname,
|
||||
Username: &username,
|
||||
Password: &password,
|
||||
}
|
||||
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts)
|
||||
defer web.CloseBody(resp)
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
|
|
@ -708,21 +719,23 @@ func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, g
|
|||
return decrypted, nil
|
||||
}
|
||||
|
||||
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, *SendEndorsementCache, error) {
|
||||
cached, endorsement, ok := cli.GroupCache.Get(gid)
|
||||
if ok && cached.Revision >= revision {
|
||||
return cached, endorsement, nil
|
||||
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, error) {
|
||||
cli.initGroupCache()
|
||||
|
||||
lastFetched, ok := cli.GroupCache.lastFetched[gid]
|
||||
if ok && time.Since(lastFetched) < 1*time.Hour {
|
||||
group, ok := cli.GroupCache.groups[gid]
|
||||
if ok && group.Revision >= revision {
|
||||
return group, nil
|
||||
}
|
||||
}
|
||||
group, err := cli.fetchGroupByID(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
cached, endorsement, ok = cli.GroupCache.Get(gid)
|
||||
if !ok {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Group not found in cache after fetching")
|
||||
return group, nil, nil
|
||||
}
|
||||
return cached, endorsement, nil
|
||||
cli.GroupCache.groups[gid] = group
|
||||
cli.GroupCache.lastFetched[gid] = time.Now()
|
||||
return group, nil
|
||||
}
|
||||
|
||||
// We should store the group master key in the group store as soon as we see it,
|
||||
|
|
@ -740,6 +753,40 @@ func (cli *Client) StoreMasterKey(ctx context.Context, groupMasterKey types.Seri
|
|||
return groupIdentifier, nil
|
||||
}
|
||||
|
||||
// We need to track active calls so we don't send too many IncomingSignalMessageCalls
|
||||
// Of course for group calls Signal doesn't tell us *anything* so we're mostly just inferring
|
||||
// So we just jam a new call ID in, and return true if we *think* this is a new incoming call
|
||||
func (cli *Client) UpdateActiveCalls(gid types.GroupIdentifier, callID string) (isActive bool) {
|
||||
cli.initGroupCache()
|
||||
// Check to see if we currently have an active call for this group
|
||||
currentCallID, ok := cli.GroupCache.activeCalls[gid]
|
||||
if ok {
|
||||
// If we do, then this must be ending the call
|
||||
if currentCallID == callID {
|
||||
delete(cli.GroupCache.activeCalls, gid)
|
||||
return false
|
||||
}
|
||||
}
|
||||
cli.GroupCache.activeCalls[gid] = callID
|
||||
return true
|
||||
}
|
||||
|
||||
func (cli *Client) initGroupCache() {
|
||||
if cli.GroupCache == nil {
|
||||
cli.GroupCache = &GroupCache{
|
||||
groups: make(map[types.GroupIdentifier]*Group),
|
||||
lastFetched: make(map[types.GroupIdentifier]time.Time),
|
||||
activeCalls: make(map[types.GroupIdentifier]string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GroupCache struct {
|
||||
groups map[types.GroupIdentifier]*Group
|
||||
lastFetched map[types.GroupIdentifier]time.Time
|
||||
activeCalls map[types.GroupIdentifier]string
|
||||
}
|
||||
|
||||
func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) {
|
||||
masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey)
|
||||
groupMasterKey := masterKeyFromBytes(masterKeyBytes)
|
||||
|
|
@ -757,15 +804,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger()
|
||||
serverSignature := encryptedGroupChange.ServerSignature
|
||||
encryptedActionsBytes := encryptedGroupChange.Actions
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
rawGroupID, _ := masterKeyToBytes(groupMasterKey).GroupIdentifier()
|
||||
if rawGroupID != nil {
|
||||
cli.GroupCache.Delete(types.GroupIdentifier(rawGroupID.String()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
if verifySignature {
|
||||
|
|
@ -788,14 +826,14 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
return nil, err
|
||||
}
|
||||
|
||||
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceUserId))
|
||||
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't decrypt source serviceID")
|
||||
return nil, err
|
||||
}
|
||||
decryptedGroupChange := &GroupChange{
|
||||
GroupMasterKey: groupMasterKey,
|
||||
Revision: encryptedActions.Version,
|
||||
Revision: encryptedActions.Revision,
|
||||
SourceServiceID: sourceServiceID,
|
||||
}
|
||||
|
||||
|
|
@ -815,7 +853,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description)
|
||||
if err == nil {
|
||||
// treat a failure in obtaining the description as non-fatal
|
||||
newDescription := cleanupStringProperty(descriptionBlob.GetDescriptionText())
|
||||
newDescription := cleanupStringProperty(descriptionBlob.GetDescription())
|
||||
decryptedGroupChange.ModifyDescription = &newDescription
|
||||
}
|
||||
}
|
||||
|
|
@ -890,7 +928,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
}
|
||||
}
|
||||
|
||||
for _, addPendingMember := range encryptedActions.AddMembersPendingProfileKey {
|
||||
for _, addPendingMember := range encryptedActions.AddPendingMembers {
|
||||
if addPendingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -903,7 +941,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember)
|
||||
}
|
||||
|
||||
for _, deletePendingMember := range encryptedActions.DeleteMembersPendingProfileKey {
|
||||
for _, deletePendingMember := range encryptedActions.DeletePendingMembers {
|
||||
if deletePendingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -916,7 +954,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID)
|
||||
}
|
||||
|
||||
for _, promotePendingMember := range encryptedActions.PromoteMembersPendingProfileKey {
|
||||
for _, promotePendingMember := range encryptedActions.PromotePendingMembers {
|
||||
if promotePendingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -935,7 +973,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
}
|
||||
}
|
||||
|
||||
for _, promotePendingPniAciMember := range encryptedActions.PromoteMembersPendingPniAciProfileKey {
|
||||
for _, promotePendingPniAciMember := range encryptedActions.PromotePendingPniAciMembers {
|
||||
// TODO: pretending this is a PendingMember should do for mautrix-signal, but we probably want to treat them separately at some point
|
||||
if promotePendingPniAciMember == nil {
|
||||
continue
|
||||
|
|
@ -965,7 +1003,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
}
|
||||
}
|
||||
|
||||
for _, addRequestingMember := range encryptedActions.AddMembersPendingAdminApproval {
|
||||
for _, addRequestingMember := range encryptedActions.AddRequestingMembers {
|
||||
if addRequestingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -981,7 +1019,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
}
|
||||
}
|
||||
|
||||
for _, deleteRequestingMember := range encryptedActions.DeleteMembersPendingAdminApproval {
|
||||
for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers {
|
||||
if deleteRequestingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -994,7 +1032,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID)
|
||||
}
|
||||
|
||||
for _, promoteRequestingMember := range encryptedActions.PromoteMembersPendingAdminApproval {
|
||||
for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers {
|
||||
if promoteRequestingMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1010,7 +1048,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
})
|
||||
}
|
||||
|
||||
for _, addBannedMember := range encryptedActions.AddMembersBanned {
|
||||
for _, addBannedMember := range encryptedActions.AddBannedMembers {
|
||||
if addBannedMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1027,7 +1065,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
})
|
||||
}
|
||||
|
||||
for _, deleteBannedMember := range encryptedActions.DeleteMembersBanned {
|
||||
for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers {
|
||||
if deleteBannedMember == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1055,8 +1093,8 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
if encryptedActions.ModifyAnnouncementsOnly != nil {
|
||||
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
|
||||
}
|
||||
if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 {
|
||||
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer)
|
||||
if encryptedActions.ModifyDisappearingMessagesTimer != nil && len(encryptedActions.ModifyDisappearingMessagesTimer.Timer) > 0 {
|
||||
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessagesTimer.Timer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1068,12 +1106,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
|
|||
decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword
|
||||
}
|
||||
|
||||
success = true
|
||||
err = cli.GroupCache.ApplyUpdate(decryptedGroupChange, nil)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to apply group change to cache")
|
||||
}
|
||||
|
||||
return decryptedGroupChange, nil
|
||||
}
|
||||
|
||||
|
|
@ -1127,11 +1159,11 @@ func decryptMember(ctx context.Context, member *signalpb.Member, groupSecretPara
|
|||
ACI: *aci,
|
||||
ProfileKey: *profileKey,
|
||||
Role: GroupMemberRole(member.Role),
|
||||
JoinedAtRevision: member.JoinedAtVersion,
|
||||
JoinedAtRevision: member.JoinedAtRevision,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPendingProfileKey, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
|
||||
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.PendingMember, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId)
|
||||
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
|
||||
|
|
@ -1154,7 +1186,7 @@ func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPen
|
|||
}, nil
|
||||
}
|
||||
|
||||
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.MemberPendingAdminApproval, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
|
||||
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.RequestingMember, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
|
||||
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -1175,7 +1207,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Could not get groupSecretParams from master key")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions := &signalpb.GroupChange_Actions{Version: decryptedGroupChange.Revision}
|
||||
groupChangeActions := &signalpb.GroupChange_Actions{Revision: decryptedGroupChange.Revision}
|
||||
if decryptedGroupChange.ModifyTitle != nil {
|
||||
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}}
|
||||
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
|
||||
|
|
@ -1186,7 +1218,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle}
|
||||
}
|
||||
if decryptedGroupChange.ModifyDescription != nil {
|
||||
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: *decryptedGroupChange.ModifyDescription}}
|
||||
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: *decryptedGroupChange.ModifyDescription}}
|
||||
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Could not get encrypt description")
|
||||
|
|
@ -1208,7 +1240,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
JoinFromInviteLink: addMember.JoinFromInviteLink,
|
||||
})
|
||||
} else {
|
||||
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{
|
||||
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
|
||||
Added: encryptedPendingMember,
|
||||
})
|
||||
}
|
||||
|
|
@ -1240,7 +1272,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Failed to encrypt pendingMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{
|
||||
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
|
||||
Added: encryptedPendingMember,
|
||||
})
|
||||
}
|
||||
|
|
@ -1250,7 +1282,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Encrypt UserId error for deletePendingMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.DeleteMembersPendingProfileKey = append(groupChangeActions.DeleteMembersPendingProfileKey, &signalpb.GroupChange_Actions_DeleteMemberPendingProfileKeyAction{
|
||||
groupChangeActions.DeletePendingMembers = append(groupChangeActions.DeletePendingMembers, &signalpb.GroupChange_Actions_DeletePendingMemberAction{
|
||||
DeletedUserId: encryptedUserID[:],
|
||||
})
|
||||
}
|
||||
|
|
@ -1268,7 +1300,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.PromoteMembersPendingProfileKey = append(groupChangeActions.PromoteMembersPendingProfileKey, &signalpb.GroupChange_Actions_PromoteMemberPendingProfileKeyAction{
|
||||
groupChangeActions.PromotePendingMembers = append(groupChangeActions.PromotePendingMembers, &signalpb.GroupChange_Actions_PromotePendingMemberAction{
|
||||
Presentation: *presentation,
|
||||
})
|
||||
}
|
||||
|
|
@ -1286,8 +1318,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.AddMembersPendingAdminApproval = append(groupChangeActions.AddMembersPendingAdminApproval, &signalpb.GroupChange_Actions_AddMemberPendingAdminApprovalAction{
|
||||
Added: &signalpb.MemberPendingAdminApproval{
|
||||
groupChangeActions.AddRequestingMembers = append(groupChangeActions.AddRequestingMembers, &signalpb.GroupChange_Actions_AddRequestingMemberAction{
|
||||
Added: &signalpb.RequestingMember{
|
||||
Presentation: *presentation,
|
||||
},
|
||||
})
|
||||
|
|
@ -1298,7 +1330,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.DeleteMembersPendingAdminApproval = append(groupChangeActions.DeleteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_DeleteMemberPendingAdminApprovalAction{
|
||||
groupChangeActions.DeleteRequestingMembers = append(groupChangeActions.DeleteRequestingMembers, &signalpb.GroupChange_Actions_DeleteRequestingMemberAction{
|
||||
DeletedUserId: encryptedUserID[:],
|
||||
})
|
||||
}
|
||||
|
|
@ -1309,7 +1341,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
return nil, err
|
||||
}
|
||||
|
||||
groupChangeActions.PromoteMembersPendingAdminApproval = append(groupChangeActions.PromoteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_PromoteMemberPendingAdminApprovalAction{
|
||||
groupChangeActions.PromoteRequestingMembers = append(groupChangeActions.PromoteRequestingMembers, &signalpb.GroupChange_Actions_PromoteRequestingMemberAction{
|
||||
UserId: encryptedUserID[:],
|
||||
Role: signalpb.Member_Role(promoteRequestingMember.Role),
|
||||
})
|
||||
|
|
@ -1320,8 +1352,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.AddMembersBanned = append(groupChangeActions.AddMembersBanned, &signalpb.GroupChange_Actions_AddMemberBannedAction{
|
||||
Added: &signalpb.MemberBanned{
|
||||
groupChangeActions.AddBannedMembers = append(groupChangeActions.AddBannedMembers, &signalpb.GroupChange_Actions_AddBannedMemberAction{
|
||||
Added: &signalpb.BannedMember{
|
||||
UserId: encryptedUserID[:],
|
||||
Timestamp: addBannedMember.Timestamp,
|
||||
},
|
||||
|
|
@ -1333,7 +1365,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.DeleteMembersBanned = append(groupChangeActions.DeleteMembersBanned, &signalpb.GroupChange_Actions_DeleteMemberBannedAction{
|
||||
groupChangeActions.DeleteBannedMembers = append(groupChangeActions.DeleteBannedMembers, &signalpb.GroupChange_Actions_DeleteBannedMemberAction{
|
||||
DeletedUserId: encryptedUserID[:],
|
||||
})
|
||||
}
|
||||
|
|
@ -1364,7 +1396,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
log.Err(err).Msg("Could not get encrypt Title")
|
||||
return nil, err
|
||||
}
|
||||
groupChangeActions.ModifyDisappearingMessageTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessageTimerAction{Timer: *encryptedTimer}
|
||||
groupChangeActions.ModifyDisappearingMessagesTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessagesTimerAction{Timer: *encryptedTimer}
|
||||
}
|
||||
if decryptedGroupChange.ModifyInviteLinkPassword != nil {
|
||||
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword)
|
||||
|
|
@ -1379,7 +1411,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
|
|||
return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil)
|
||||
}
|
||||
|
||||
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.MemberPendingProfileKey, error) {
|
||||
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.PendingMember, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI)
|
||||
if err != nil {
|
||||
|
|
@ -1407,7 +1439,7 @@ func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, group
|
|||
return &encryptedMember, nil, nil
|
||||
}
|
||||
|
||||
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.MemberPendingProfileKey, error) {
|
||||
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.PendingMember, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID)
|
||||
if err != nil {
|
||||
|
|
@ -1419,7 +1451,7 @@ func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *Pend
|
|||
log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember")
|
||||
return nil, err
|
||||
}
|
||||
encryptedPendingMember := signalpb.MemberPendingProfileKey{
|
||||
encryptedPendingMember := signalpb.PendingMember{
|
||||
AddedByUserId: encryptedAddedByUserID[:],
|
||||
Member: &signalpb.Member{
|
||||
UserId: encryptedUserID[:],
|
||||
|
|
@ -1472,9 +1504,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
|
|||
Password: &groupAuth.Password,
|
||||
ContentType: web.ContentTypeProtobuf,
|
||||
Body: requestBody,
|
||||
Host: web.StorageHostname,
|
||||
}
|
||||
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPatch, path, opts)
|
||||
defer web.CloseBody(resp)
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodPatch, path, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SendRequest error: %w", err)
|
||||
}
|
||||
|
|
@ -1513,21 +1545,17 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
|
|||
return &changeResp, nil
|
||||
}
|
||||
|
||||
var ErrGroupMasterKeyNotFound = errors.New("group master key not found in store")
|
||||
|
||||
func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
|
||||
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get master key for group: %w", err)
|
||||
} else if groupMasterKey == "" {
|
||||
return 0, ErrGroupMasterKeyNotFound
|
||||
}
|
||||
groupChange.GroupMasterKey = groupMasterKey
|
||||
masterKeyBytes := masterKeyToBytes(groupMasterKey)
|
||||
var refetchedAddMemberCredentials bool
|
||||
var signedGroupChange *signalpb.GroupChangeResponse
|
||||
group, _, err := cli.RetrieveGroupByID(ctx, gid, 0)
|
||||
group, err := cli.RetrieveGroupByID(ctx, gid, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch group info to update: %w", err)
|
||||
}
|
||||
|
|
@ -1550,8 +1578,10 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
|
|||
return 0, fmt.Errorf("failed to update group: %w", err)
|
||||
}
|
||||
} else if errors.Is(err, ConflictError) {
|
||||
cli.GroupCache.Delete(gid)
|
||||
group, _, err = cli.RetrieveGroupByID(ctx, gid, 0)
|
||||
delete(cli.GroupCache.groups, gid)
|
||||
delete(cli.GroupCache.lastFetched, gid)
|
||||
delete(cli.GroupCache.activeCalls, gid)
|
||||
group, err = cli.RetrieveGroupByID(ctx, gid, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch group after conflict: %w", err)
|
||||
}
|
||||
|
|
@ -1564,13 +1594,12 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
|
|||
return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err)
|
||||
}
|
||||
}
|
||||
delete(cli.GroupCache.groups, gid)
|
||||
delete(cli.GroupCache.lastFetched, gid)
|
||||
delete(cli.GroupCache.activeCalls, gid)
|
||||
if signedGroupChange == nil {
|
||||
return 0, fmt.Errorf("no signed group change returned: %w", err)
|
||||
}
|
||||
err = cli.GroupCache.ApplyUpdate(groupChange, signedGroupChange.GroupSendEndorsementsResponse)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to apply group change to cache")
|
||||
}
|
||||
groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to marshal signed group change: %w", err)
|
||||
|
|
@ -1603,12 +1632,12 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
|
|||
encryptedGroup := &signalpb.Group{
|
||||
PublicKey: groupPublicParams[:],
|
||||
Title: *encryptedTitle,
|
||||
AvatarUrl: decryptedGroup.AvatarPath,
|
||||
Avatar: decryptedGroup.AvatarPath,
|
||||
AnnouncementsOnly: decryptedGroup.AnnouncementsOnly,
|
||||
Version: 0,
|
||||
Revision: 0,
|
||||
}
|
||||
if decryptedGroup.Description != "" {
|
||||
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: decryptedGroup.Description}}
|
||||
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: decryptedGroup.Description}}
|
||||
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Could not get encrypt Description")
|
||||
|
|
@ -1634,7 +1663,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
|
|||
if encryptedMember != nil {
|
||||
encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember)
|
||||
} else {
|
||||
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember)
|
||||
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
|
||||
}
|
||||
}
|
||||
for _, pendingMember := range decryptedGroup.PendingMembers {
|
||||
|
|
@ -1643,7 +1672,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
|
|||
log.Err(err).Msg("Failed to encrypt pendingMember")
|
||||
return nil, err
|
||||
}
|
||||
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember)
|
||||
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
|
||||
}
|
||||
return encryptedGroup, nil
|
||||
}
|
||||
|
|
@ -1666,7 +1695,7 @@ func PrepareGroupCreation(decryptedGroup *Group) (libsignalgo.GroupMasterKey, er
|
|||
return masterKeyBytes, nil
|
||||
}
|
||||
|
||||
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group) (*Group, error) {
|
||||
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger()
|
||||
masterKeyBytes, err := PrepareGroupCreation(decryptedGroup)
|
||||
if err != nil {
|
||||
|
|
@ -1681,6 +1710,14 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
|
|||
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
|
||||
return nil, err
|
||||
}
|
||||
if len(avatarBytes) > 0 {
|
||||
avatarPath, err := cli.UploadGroupAvatar(ctx, avatarBytes, decryptedGroup.GroupIdentifier)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to upload group avatar")
|
||||
return nil, err
|
||||
}
|
||||
decryptedGroup.AvatarPath = avatarPath
|
||||
}
|
||||
encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to encrypt group")
|
||||
|
|
@ -1702,9 +1739,9 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
|
|||
Password: &groupAuth.Password,
|
||||
ContentType: web.ContentTypeProtobuf,
|
||||
Body: requestBody,
|
||||
Host: web.StorageHostname,
|
||||
}
|
||||
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPut, path, opts)
|
||||
defer web.CloseBody(resp)
|
||||
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, path, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SendRequest error: %w", err)
|
||||
}
|
||||
|
|
@ -1731,9 +1768,9 @@ func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
|
|||
return InviteLinkPasswordFromBytes(random.Bytes(16))
|
||||
}
|
||||
|
||||
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group) (*Group, error) {
|
||||
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroup").Logger()
|
||||
group, err := cli.createGroupOnServer(ctx, decryptedGroup)
|
||||
group, err := cli.createGroupOnServer(ctx, decryptedGroup, avatarBytes)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error creating group on server")
|
||||
return nil, err
|
||||
|
|
@ -1756,7 +1793,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
|
|||
return nil, err
|
||||
}
|
||||
if groupMasterKey == "" {
|
||||
return nil, ErrGroupMasterKeyNotFound
|
||||
return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
|
||||
}
|
||||
masterKeyBytes := masterKeyToBytes(groupMasterKey)
|
||||
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
|
||||
|
|
@ -1767,6 +1804,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
|
|||
Username: &groupAuth.Username,
|
||||
Password: &groupAuth.Password,
|
||||
ContentType: web.ContentTypeProtobuf,
|
||||
Host: web.StorageHostname,
|
||||
Headers: map[string]string{
|
||||
// TODO actually cache the data and provide real expiry timestamp
|
||||
"Cached-Send-Endorsements": "0",
|
||||
|
|
@ -1774,13 +1812,12 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
|
|||
}
|
||||
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false
|
||||
path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState)
|
||||
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts)
|
||||
defer web.CloseBody(response)
|
||||
response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected response status: %d", response.StatusCode)
|
||||
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
|
||||
}
|
||||
var encryptedGroupChanges signalpb.GroupChanges
|
||||
groupChangesBytes, err := io.ReadAll(response.Body)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
272
pkg/signalmeow/protobuf/ContactDiscovery.pb.go
generated
Normal file
272
pkg/signalmeow/protobuf/ContactDiscovery.pb.go
generated
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v3.21.12
|
||||
// source: ContactDiscovery.proto
|
||||
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package signalpb
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type CDSClientRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
|
||||
// by its 16-byte UAK.
|
||||
AciUakPairs []byte `protobuf:"bytes,1,opt,name=aci_uak_pairs,json=aciUakPairs" json:"aci_uak_pairs,omitempty"`
|
||||
// Each E164 is an 8-byte big-endian number, as 8 bytes.
|
||||
PrevE164S []byte `protobuf:"bytes,2,opt,name=prev_e164s,json=prevE164s" json:"prev_e164s,omitempty"`
|
||||
NewE164S []byte `protobuf:"bytes,3,opt,name=new_e164s,json=newE164s" json:"new_e164s,omitempty"`
|
||||
DiscardE164S []byte `protobuf:"bytes,4,opt,name=discard_e164s,json=discardE164s" json:"discard_e164s,omitempty"`
|
||||
// If true, the client has more pairs or e164s to send. If false or unset,
|
||||
// this is the client's last request, and processing should commence.
|
||||
HasMore *bool `protobuf:"varint,5,opt,name=has_more,json=hasMore" json:"has_more,omitempty"`
|
||||
// If set, a token which allows rate limiting to discount the e164s in
|
||||
// the request's prev_e164s, only counting new_e164s. If not set, then
|
||||
// rate limiting considers both prev_e164s' and new_e164s' size.
|
||||
Token []byte `protobuf:"bytes,6,opt,name=token" json:"token,omitempty"`
|
||||
// After receiving a new token from the server, send back a message just
|
||||
// containing a token_ack.
|
||||
TokenAck *bool `protobuf:"varint,7,opt,name=token_ack,json=tokenAck" json:"token_ack,omitempty"`
|
||||
// Request that, if the server allows, both ACI and PNI be returned even
|
||||
// if the aci_uak_pairs don't match.
|
||||
ReturnAcisWithoutUaks *bool `protobuf:"varint,8,opt,name=return_acis_without_uaks,json=returnAcisWithoutUaks" json:"return_acis_without_uaks,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) Reset() {
|
||||
*x = CDSClientRequest{}
|
||||
mi := &file_ContactDiscovery_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CDSClientRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CDSClientRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_ContactDiscovery_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CDSClientRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CDSClientRequest) Descriptor() ([]byte, []int) {
|
||||
return file_ContactDiscovery_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetAciUakPairs() []byte {
|
||||
if x != nil {
|
||||
return x.AciUakPairs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetPrevE164S() []byte {
|
||||
if x != nil {
|
||||
return x.PrevE164S
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetNewE164S() []byte {
|
||||
if x != nil {
|
||||
return x.NewE164S
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetDiscardE164S() []byte {
|
||||
if x != nil {
|
||||
return x.DiscardE164S
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetHasMore() bool {
|
||||
if x != nil && x.HasMore != nil {
|
||||
return *x.HasMore
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetToken() []byte {
|
||||
if x != nil {
|
||||
return x.Token
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetTokenAck() bool {
|
||||
if x != nil && x.TokenAck != nil {
|
||||
return *x.TokenAck
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CDSClientRequest) GetReturnAcisWithoutUaks() bool {
|
||||
if x != nil && x.ReturnAcisWithoutUaks != nil {
|
||||
return *x.ReturnAcisWithoutUaks
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type CDSClientResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
|
||||
// If the e164 was not found, PNI and ACI are all zeros. If the PNI
|
||||
// was found but the ACI was not, the PNI will be non-zero and the ACI
|
||||
// will be all zeros. ACI will be returned if one of the returned
|
||||
// PNIs has an ACI/UAK pair that matches.
|
||||
//
|
||||
// Should the request be successful (IE: a successful status returned),
|
||||
// |e164_pni_aci_triple| will always equal |e164| of the request,
|
||||
// so the entire marshalled size of the response will be (2+32)*|e164|,
|
||||
// where the additional 2 bytes are the id/type/length additions of the
|
||||
// protobuf marshaling added to each byte array. This avoids any data
|
||||
// leakage based on the size of the encrypted output.
|
||||
E164PniAciTriples []byte `protobuf:"bytes,1,opt,name=e164_pni_aci_triples,json=e164PniAciTriples" json:"e164_pni_aci_triples,omitempty"`
|
||||
// A token which allows subsequent calls' rate limiting to discount the
|
||||
// e164s sent up in this request, only counting those in the next
|
||||
// request's new_e164s.
|
||||
Token []byte `protobuf:"bytes,3,opt,name=token" json:"token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CDSClientResponse) Reset() {
|
||||
*x = CDSClientResponse{}
|
||||
mi := &file_ContactDiscovery_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CDSClientResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CDSClientResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CDSClientResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_ContactDiscovery_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CDSClientResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CDSClientResponse) Descriptor() ([]byte, []int) {
|
||||
return file_ContactDiscovery_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CDSClientResponse) GetE164PniAciTriples() []byte {
|
||||
if x != nil {
|
||||
return x.E164PniAciTriples
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CDSClientResponse) GetToken() []byte {
|
||||
if x != nil {
|
||||
return x.Token
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_ContactDiscovery_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_ContactDiscovery_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16ContactDiscovery.proto\x12\rsignalservice\"\x9e\x02\n" +
|
||||
"\x10CDSClientRequest\x12\"\n" +
|
||||
"\raci_uak_pairs\x18\x01 \x01(\fR\vaciUakPairs\x12\x1d\n" +
|
||||
"\n" +
|
||||
"prev_e164s\x18\x02 \x01(\fR\tprevE164s\x12\x1b\n" +
|
||||
"\tnew_e164s\x18\x03 \x01(\fR\bnewE164s\x12#\n" +
|
||||
"\rdiscard_e164s\x18\x04 \x01(\fR\fdiscardE164s\x12\x19\n" +
|
||||
"\bhas_more\x18\x05 \x01(\bR\ahasMore\x12\x14\n" +
|
||||
"\x05token\x18\x06 \x01(\fR\x05token\x12\x1b\n" +
|
||||
"\ttoken_ack\x18\a \x01(\bR\btokenAck\x127\n" +
|
||||
"\x18return_acis_without_uaks\x18\b \x01(\bR\x15returnAcisWithoutUaks\"Z\n" +
|
||||
"\x11CDSClientResponse\x12/\n" +
|
||||
"\x14e164_pni_aci_triples\x18\x01 \x01(\fR\x11e164PniAciTriples\x12\x14\n" +
|
||||
"\x05token\x18\x03 \x01(\fR\x05token"
|
||||
|
||||
var (
|
||||
file_ContactDiscovery_proto_rawDescOnce sync.Once
|
||||
file_ContactDiscovery_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_ContactDiscovery_proto_rawDescGZIP() []byte {
|
||||
file_ContactDiscovery_proto_rawDescOnce.Do(func() {
|
||||
file_ContactDiscovery_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ContactDiscovery_proto_rawDesc), len(file_ContactDiscovery_proto_rawDesc)))
|
||||
})
|
||||
return file_ContactDiscovery_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_ContactDiscovery_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_ContactDiscovery_proto_goTypes = []any{
|
||||
(*CDSClientRequest)(nil), // 0: signalservice.CDSClientRequest
|
||||
(*CDSClientResponse)(nil), // 1: signalservice.CDSClientResponse
|
||||
}
|
||||
var file_ContactDiscovery_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_ContactDiscovery_proto_init() }
|
||||
func file_ContactDiscovery_proto_init() {
|
||||
if File_ContactDiscovery_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ContactDiscovery_proto_rawDesc), len(file_ContactDiscovery_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_ContactDiscovery_proto_goTypes,
|
||||
DependencyIndexes: file_ContactDiscovery_proto_depIdxs,
|
||||
MessageInfos: file_ContactDiscovery_proto_msgTypes,
|
||||
}.Build()
|
||||
File_ContactDiscovery_proto = out.File
|
||||
file_ContactDiscovery_proto_goTypes = nil
|
||||
file_ContactDiscovery_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,36 +1,37 @@
|
|||
syntax = "proto3";
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package org.signal.cdsi;
|
||||
package signalservice;
|
||||
|
||||
message ClientRequest {
|
||||
message CDSClientRequest {
|
||||
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
|
||||
// by its 16-byte UAK.
|
||||
bytes aci_uak_pairs = 1;
|
||||
optional bytes aci_uak_pairs = 1;
|
||||
|
||||
// Each E164 is an 8-byte big-endian number, as 8 bytes.
|
||||
bytes prev_e164s = 2;
|
||||
bytes new_e164s = 3;
|
||||
bytes discard_e164s = 4;
|
||||
optional bytes prev_e164s = 2;
|
||||
optional bytes new_e164s = 3;
|
||||
optional bytes discard_e164s = 4;
|
||||
|
||||
// If true, the client has more pairs or e164s to send. If false or unset,
|
||||
// this is the client's last request, and processing should commence.
|
||||
// bool has_more = 5;
|
||||
reserved 5;
|
||||
optional bool has_more = 5;
|
||||
|
||||
// If set, a token which allows rate limiting to discount the e164s in
|
||||
// the request's prev_e164s, only counting new_e164s. If not set, then
|
||||
// rate limiting considers both prev_e164s' and new_e164s' size.
|
||||
bytes token = 6;
|
||||
optional bytes token = 6;
|
||||
|
||||
// After receiving a new token from the server, send back a message just
|
||||
// containing a token_ack.
|
||||
bool token_ack = 7;
|
||||
optional bool token_ack = 7;
|
||||
|
||||
// [deprecated] bool return_acis_without_uaks = 8
|
||||
reserved 8;
|
||||
// Request that, if the server allows, both ACI and PNI be returned even
|
||||
// if the aci_uak_pairs don't match.
|
||||
optional bool return_acis_without_uaks = 8;
|
||||
}
|
||||
|
||||
message ClientResponse {
|
||||
message CDSClientResponse {
|
||||
// Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
|
||||
// If the e164 was not found, PNI and ACI are all zeros. If the PNI
|
||||
// was found but the ACI was not, the PNI will be non-zero and the ACI
|
||||
|
|
@ -43,18 +44,10 @@ message ClientResponse {
|
|||
// where the additional 2 bytes are the id/type/length additions of the
|
||||
// protobuf marshaling added to each byte array. This avoids any data
|
||||
// leakage based on the size of the encrypted output.
|
||||
bytes e164_pni_aci_triples = 1;
|
||||
|
||||
// int32 retry_after_secs = 2 [deprecated]
|
||||
reserved 2;
|
||||
optional bytes e164_pni_aci_triples = 1;
|
||||
|
||||
// A token which allows subsequent calls' rate limiting to discount the
|
||||
// e164s sent up in this request, only counting those in the next
|
||||
// request's new_e164s.
|
||||
bytes token = 3;
|
||||
|
||||
// On a successful response to a token_ack request, the number of permits
|
||||
// that were deducted from the user's rate-limit in order to process the
|
||||
// request
|
||||
int32 debug_permits_used = 4;
|
||||
optional bytes token = 3;
|
||||
}
|
||||
4
pkg/signalmeow/protobuf/DeviceName.pb.go
generated
4
pkg/signalmeow/protobuf/DeviceName.pb.go
generated
|
|
@ -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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue