├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug.md
│ ├── config.yml
│ └── enhancement.md
└── workflows
│ ├── go.yml
│ └── stale.yml
├── .gitignore
├── .gitlab-ci.yml
├── .gitmodules
├── .idea
└── icon.svg
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── Dockerfile
├── Dockerfile.ci
├── LICENSE
├── LICENSE.exceptions
├── README.md
├── ROADMAP.md
├── build-go.sh
├── build-rust.sh
├── build.sh
├── cmd
└── mautrix-signal
│ ├── legacymigrate.go
│ ├── legacymigrate.sql
│ ├── legacyprovision.go
│ └── main.go
├── docker-run.sh
├── go.mod
├── go.sum
└── pkg
├── connector
├── backfill.go
├── capabilities.go
├── chatinfo.go
├── chatsync.go
├── client.go
├── config.go
├── connector.go
├── dbmeta.go
├── directmedia.go
├── example-config.yaml
├── groupinfo.go
├── handlematrix.go
├── handlesignal.go
├── id.go
└── login.go
├── libsignalgo
├── README.md
├── accountentropy.go
├── address.go
├── address_test.go
├── aes256gcmsiv.go
├── aes256gcmsiv_test.go
├── authcredential.go
├── backupkey.go
├── buffer.go
├── cflags.go
├── ciphertextmessage.go
├── conversions.go
├── decryptionerrormessage.go
├── devicetransfer.go
├── devicetransfer_test.go
├── error.go
├── fingerprint.go
├── fingerprint_test.go
├── groupcipher.go
├── groupcipher_test.go
├── groupsecretparams.go
├── hsmenclave.go
├── hsmenclave_test.go
├── identitykey.go
├── identitykey_test.go
├── identitykeystore.go
├── inmemorystore_test.go
├── kdf.go
├── kdf_test.go
├── kyberprekey.go
├── kyberprekeystore.go
├── libsignal-ffi.h
├── logging.go
├── message.go
├── messagebackupkey.go
├── nocopy.go
├── plaintextcontent.go
├── prekey.go
├── prekeybundle.go
├── prekeymessage.go
├── prekeystore.go
├── privatekey.go
├── privatekey_test.go
├── profilekey.go
├── publickey.go
├── resources
│ └── clienthandshakestart.data
├── sealedsender.go
├── sendercertificate.go
├── sendercertificate_test.go
├── senderkeydistributionmessage.go
├── senderkeyrecord.go
├── senderkeystore.go
├── serializedeserializeroundtrip_test.go
├── servercertificate.go
├── serverpublicparams.go
├── serviceid.go
├── serviceid_clang.go
├── serviceid_gcc.go
├── session_test.go
├── sessionrecord.go
├── sessionstore.go
├── setup_test.go
├── sgxclient.go
├── signedprekey.go
├── signedprekeystore.go
├── storeutil.go
├── update-ffi.sh
└── version.go
├── msgconv
├── from-matrix.go
├── from-signal-backup.go
├── from-signal.go
├── matrixfmt
│ ├── convert.go
│ ├── convert_test.go
│ └── html.go
├── msgconv.go
├── signalfmt
│ ├── convert.go
│ ├── convert_test.go
│ ├── html.go
│ ├── tags.go
│ └── tree.go
└── urlpreview.go
├── signalid
├── dbmeta.go
├── ids.go
└── media.go
└── signalmeow
├── attachments.go
├── attachments_stream.go
├── backup.go
├── client.go
├── contact.go
├── contactdiscovery.go
├── devicename.go
├── events
└── message.go
├── groups.go
├── keys.go
├── misc.go
├── prod-server-public-params.dat
├── profile.go
├── protobuf
├── ContactDiscovery.pb.go
├── ContactDiscovery.proto
├── DeviceName.pb.go
├── DeviceName.proto
├── Groups.pb.go
├── Groups.proto
├── Provisioning.pb.go
├── Provisioning.proto
├── SignalService.pb.go
├── SignalService.proto
├── StickerResources.pb.go
├── StickerResources.proto
├── StorageService.pb.go
├── StorageService.proto
├── UnidentifiedDelivery.pb.go
├── UnidentifiedDelivery.proto
├── WebSocketResources.pb.go
├── WebSocketResources.proto
├── backuppb
│ ├── Backup.pb.go
│ ├── Backup.pb.raw
│ └── Backup.proto
├── build-protos.sh
├── extra.go
└── update-protos.sh
├── provisioning.go
├── provisioning_cipher.go
├── pushreg.go
├── receiving.go
├── receiving_decrypt.go
├── sending.go
├── serviceauth.go
├── storageservice.go
├── store
├── backup_store.go
├── container.go
├── device.go
├── event_buffer.go
├── group_store.go
├── identity_store.go
├── prekey_store.go
├── profile_key_store.go
├── recipient_store.go
├── sender_key_store.go
├── session_store.go
└── upgrades
│ ├── 00-latest.sql
│ ├── 02-groups.sql
│ ├── 03-contacts.sql
│ ├── 04-kyber-prekeys.sql
│ ├── 05-postgres-profile-key.sql
│ ├── 06-profile-avatar-path.sql
│ ├── 08-profile-fetch-time.sql
│ ├── 08-resync-schema-449.sql
│ ├── 09-pni-sending.sql
│ ├── 10-prekey-store-service-id.postgres.sql
│ ├── 10-prekey-store-service-id.sqlite.sql
│ ├── 11-aci-to-account-id.sql
│ ├── 12-drop-identity-key-device-id.postgres.sql
│ ├── 12-drop-identity-key-device-id.sqlite.sql
│ ├── 13-recipients-table.postgres.sql
│ ├── 13-recipients-table.sqlite.sql
│ ├── 14-save-storage-master-key.sql
│ ├── 15-needs-pni-signature.sql
│ ├── 16-remove-extra-prekeys.go
│ ├── 17-store-account-record.sql
│ ├── 18-store-backup-keys.sql
│ ├── 19-store-backup-data.sql
│ ├── 20-fix-backup-chat-columns.go
│ ├── 21-event-buffer.sql
│ └── upgrades.go
├── types.go
├── types
├── contact.go
└── identifer.go
├── web
├── signal-root.crt.der
├── signalwebsocket.go
└── web.go
└── wspb
└── wspb.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | logs
3 | start
4 | config.yaml
5 | registration.yaml
6 | *.db
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{yaml,yml,sql}]
15 | indent_style = space
16 |
17 | [{.gitlab-ci.yml,.github/workflows/*.yml}]
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pb.go linguist-generated=true
2 | *.pb.raw binary linguist-generated=true
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: If something is definitely wrong in the bridge (rather than just a setup issue),
4 | file a bug report. Remember to include relevant logs.
5 | labels: bug
6 |
7 | ---
8 |
9 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | contact_links:
2 | - name: Troubleshooting docs & FAQ
3 | url: https://docs.mau.fi/bridges/general/troubleshooting.html
4 | about: Check this first if you're having problems setting up the bridge.
5 | - name: Support room
6 | url: https://matrix.to/#/#signal:maunium.net
7 | about: For setup issues not answered by the troubleshooting docs, ask in the Matrix room.
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Enhancement request
3 | about: Submit a feature request or other suggestion
4 | labels: enhancement
5 |
6 | ---
7 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on: [push, pull_request]
4 |
5 | env:
6 | GOTOOLCHAIN: local
7 |
8 | jobs:
9 | lint:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | go-version: ["1.23", "1.24"]
15 | name: Lint ${{ matrix.go-version == '1.24' && '(latest)' || '(old)' }}
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Set up Go
21 | uses: actions/setup-go@v5
22 | with:
23 | go-version: ${{ matrix.go-version }}
24 | cache: true
25 |
26 | - name: Install libolm
27 | run: sudo apt-get install libolm-dev
28 |
29 | - name: Install dependencies
30 | run: |
31 | go install golang.org/x/tools/cmd/goimports@latest
32 | go install honnef.co/go/tools/cmd/staticcheck@latest
33 | export PATH="$HOME/go/bin:$PATH"
34 |
35 | - name: Run pre-commit
36 | uses: pre-commit/action@v3.0.1
37 |
38 | test:
39 | runs-on: ubuntu-latest
40 | strategy:
41 | fail-fast: false
42 | matrix:
43 | go-version: ["1.23", "1.24"]
44 | name: Test ${{ matrix.go-version == '1.24' && '(latest)' || '(old)' }}
45 |
46 | steps:
47 | - uses: actions/checkout@v4
48 |
49 | - name: Set up Go
50 | uses: actions/setup-go@v5
51 | with:
52 | go-version: ${{ matrix.go-version }}
53 | cache: true
54 |
55 | #- name: Set up gotestfmt
56 | # uses: GoTestTools/gotestfmt-action@v2
57 | # with:
58 | # token: ${{ secrets.GITHUB_TOKEN }}
59 |
60 | - name: Install libolm
61 | run: sudo apt-get install libolm-dev
62 |
63 | - name: Download libsignal
64 | run: |
65 | curl -L https://mau.dev/tulir/gomuks-build-docker/-/jobs/artifacts/master/raw/libsignal_ffi.a?job=libsignal%20linux%20amd64 -o libsignal_ffi.a
66 |
67 | - name: Run tests
68 | run: |
69 | set -euo pipefail
70 | export LIBRARY_PATH=.
71 | #go test -v -json ./... -cover | gotestfmt
72 | go test ./...
73 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Lock old issues'
2 |
3 | on:
4 | schedule:
5 | - cron: '0 12 * * *'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | issues: write
10 | # pull-requests: write
11 | # discussions: write
12 |
13 | concurrency:
14 | group: lock-threads
15 |
16 | jobs:
17 | lock-stale:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: dessant/lock-threads@v5
21 | id: lock
22 | with:
23 | issue-inactive-days: 90
24 | process-only: issues
25 | - name: Log processed threads
26 | run: |
27 | if [ '${{ steps.lock.outputs.issues }}' ]; then
28 | echo "Issues:" && echo '${{ steps.lock.outputs.issues }}' | jq -r '.[] | "https://github.com/\(.owner)/\(.repo)/issues/\(.issue_number)"'
29 | fi
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.yaml
2 | !example-config.yaml
3 | !.pre-commit-config.yaml
4 |
5 | *.db*
6 | *.log*
7 |
8 | /mautrix-signal
9 | /start
10 | /libsignal_ffi.a
11 |
12 | .idea
13 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | include:
2 | - project: 'mautrix/ci'
3 | file: '/gov2-as-default.yml'
4 |
5 | variables:
6 | BUILDER_IMAGE: dock.mau.dev/tulir/gomuks-build-docker/signal
7 |
8 | # 32-bit arm builds aren't supported
9 | build arm:
10 | rules:
11 | - when: never
12 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "pkg/libsignalgo/libsignal"]
2 | path = pkg/libsignalgo/libsignal
3 | url = https://github.com/signalapp/libsignal.git
4 |
--------------------------------------------------------------------------------
/.idea/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: trailing-whitespace
6 | exclude_types: [markdown]
7 | - id: end-of-file-fixer
8 | - id: check-yaml
9 | - id: check-added-large-files
10 |
11 | - repo: https://github.com/tekwizely/pre-commit-golang
12 | rev: v1.0.0-rc.1
13 | hooks:
14 | - id: go-imports
15 | exclude: "pb\\.go$"
16 | args:
17 | - "-local"
18 | - "go.mau.fi/mautrix-signal"
19 | - "-w"
20 | - id: go-vet-mod
21 | # - id: go-staticcheck-repo-mod
22 | # TODO: reenable this and fix all the problems
23 |
24 | - repo: https://github.com/beeper/pre-commit-go
25 | rev: v0.4.2
26 | hooks:
27 | - id: zerolog-ban-msgf
28 | - id: zerolog-use-stringer
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # -- Build libsignal (with Rust) --
2 | FROM rust:1-alpine as rust-builder
3 | RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
4 |
5 | WORKDIR /build
6 | # Copy all files needed for Rust build, and no Go files
7 | COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
8 | COPY build-rust.sh .
9 |
10 | ARG DBG=0
11 | RUN ./build-rust.sh
12 |
13 | # -- Build mautrix-signal (with Go) --
14 | FROM golang:1-alpine3.21 AS go-builder
15 | RUN apk add --no-cache git ca-certificates build-base olm-dev
16 |
17 | WORKDIR /build
18 | # Copy all files needed for Go build, and no Rust files
19 | COPY *.go go.* *.yaml *.sh ./
20 | COPY pkg/signalmeow/. pkg/signalmeow/.
21 | COPY pkg/libsignalgo/* pkg/libsignalgo/
22 | COPY pkg/libsignalgo/resources/. pkg/libsignalgo/resources/.
23 | COPY pkg/msgconv/. pkg/msgconv/.
24 | COPY pkg/signalid/. pkg/signalid/.
25 | COPY pkg/connector/. pkg/connector/.
26 | COPY cmd/. cmd/.
27 | COPY .git .git
28 |
29 | ARG DBG=0
30 | ENV LIBRARY_PATH=.
31 | COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./
32 | RUN <.
16 |
17 | package main
18 |
19 | import (
20 | _ "embed"
21 |
22 | up "go.mau.fi/util/configupgrade"
23 |
24 | "maunium.net/go/mautrix/bridgev2/bridgeconfig"
25 | )
26 |
27 | const legacyMigrateRenameTables = `
28 | ALTER TABLE portal RENAME TO portal_old;
29 | ALTER TABLE puppet RENAME TO puppet_old;
30 | ALTER TABLE "user" RENAME TO user_old;
31 | ALTER TABLE user_portal RENAME TO user_portal_old;
32 | ALTER TABLE message RENAME TO message_old;
33 | ALTER TABLE reaction RENAME TO reaction_old;
34 | ALTER TABLE disappearing_message RENAME TO disappearing_message_old;
35 | `
36 |
37 | //go:embed legacymigrate.sql
38 | var legacyMigrateCopyData string
39 |
40 | func migrateLegacyConfig(helper up.Helper) {
41 | helper.Set(up.Str, "mautrix.bridge.e2ee", "encryption", "pickle_key")
42 | bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "displayname_template"}, []string{"network", "displayname_template"})
43 | bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "note_to_self_avatar"}, []string{"network", "note_to_self_avatar"})
44 | bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "location_format"}, []string{"network", "location_format"})
45 | bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "use_contact_avatars"}, []string{"network", "use_contact_avatars"})
46 | bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "use_outdated_profiles"}, []string{"network", "use_outdated_profiles"})
47 | bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "number_in_topic"}, []string{"network", "number_in_topic"})
48 | bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"signal", "device_name"}, []string{"network", "device_name"})
49 | }
50 |
--------------------------------------------------------------------------------
/cmd/mautrix-signal/main.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "net/http"
21 |
22 | "maunium.net/go/mautrix/bridgev2/bridgeconfig"
23 | "maunium.net/go/mautrix/bridgev2/matrix/mxmain"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/connector"
26 | "go.mau.fi/mautrix-signal/pkg/signalmeow"
27 | )
28 |
29 | // Information to find out exactly which commit the bridge was built from.
30 | // These are filled at build time with the -X linker flag.
31 | var (
32 | Tag = "unknown"
33 | Commit = "unknown"
34 | BuildTime = "unknown"
35 | )
36 |
37 | var m = mxmain.BridgeMain{
38 | Name: "mautrix-signal",
39 | URL: "https://github.com/mautrix/signal",
40 | Description: "A Matrix-Signal puppeting bridge.",
41 | Version: "0.8.3",
42 |
43 | Connector: &connector.SignalConnector{},
44 | }
45 |
46 | func main() {
47 | bridgeconfig.HackyMigrateLegacyNetworkConfig = migrateLegacyConfig
48 | m.PostInit = func() {
49 | signalmeow.SetLogger(m.Log.With().Str("component", "signalmeow").Logger())
50 | m.CheckLegacyDB(
51 | 20,
52 | "v0.5.1",
53 | "v0.7.0",
54 | m.LegacyMigrateSimple(legacyMigrateRenameTables, legacyMigrateCopyData, 18),
55 | true,
56 | )
57 | }
58 | m.PostStart = func() {
59 | if m.Matrix.Provisioning != nil {
60 | m.Matrix.Provisioning.Router.HandleFunc("/v2/link/new", legacyProvLinkNew).Methods(http.MethodPost)
61 | m.Matrix.Provisioning.Router.HandleFunc("/v2/link/wait/scan", legacyProvLinkWaitScan).Methods(http.MethodPost)
62 | m.Matrix.Provisioning.Router.HandleFunc("/v2/link/wait/account", legacyProvLinkWaitAccount).Methods(http.MethodPost)
63 | m.Matrix.Provisioning.Router.HandleFunc("/v2/logout", legacyProvLogout).Methods(http.MethodPost)
64 | m.Matrix.Provisioning.Router.HandleFunc("/v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier).Methods(http.MethodGet)
65 | m.Matrix.Provisioning.Router.HandleFunc("/v2/pm/{phonenum}", legacyProvPM).Methods(http.MethodPost)
66 | }
67 | }
68 | m.InitVersion(Tag, Commit, BuildTime)
69 | m.Run()
70 | }
71 |
--------------------------------------------------------------------------------
/docker-run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [[ -z "$GID" ]]; then
4 | GID="$UID"
5 | fi
6 |
7 | BINARY_NAME=/usr/bin/mautrix-signal
8 |
9 | # Define functions.
10 | function fixperms {
11 | chown -R $UID:$GID /data
12 |
13 | # /opt/mautrix-signal is read-only, so disable file logging if it's pointing there.
14 | if [[ "$(yq e '.logging.writers[1].filename' /data/config.yaml)" == "./logs/mautrix-signal.log" ]]; then
15 | yq -I4 e -i 'del(.logging.writers[1])' /data/config.yaml
16 | fi
17 | }
18 |
19 | if [[ ! -f /data/config.yaml ]]; then
20 | $BINARY_NAME -c /data/config.yaml -e
21 | echo "Didn't find a config file."
22 | echo "Copied default config file to /data/config.yaml"
23 | echo "Modify that config file to your liking."
24 | echo "Start the container again after that to generate the registration file."
25 | exit
26 | fi
27 |
28 | if [[ ! -f /data/registration.yaml ]]; then
29 | $BINARY_NAME -g -c /data/config.yaml -r /data/registration.yaml || exit $?
30 | echo "Didn't find a registration file."
31 | echo "Generated one for you."
32 | echo "See https://docs.mau.fi/bridges/general/registering-appservices.html on how to use it."
33 | exit
34 | fi
35 |
36 | cd /data
37 | fixperms
38 |
39 | DLV=/usr/bin/dlv
40 | if [ -x "$DLV" ]; then
41 | if [ "$DBGWAIT" != 1 ]; then
42 | NOWAIT=1
43 | fi
44 | BINARY_NAME="${DLV} exec ${BINARY_NAME} ${NOWAIT:+--continue --accept-multiclient} --api-version 2 --headless -l :4040"
45 | fi
46 |
47 | exec su-exec $UID:$GID $BINARY_NAME
48 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module go.mau.fi/mautrix-signal
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.24.3
6 |
7 | require (
8 | github.com/coder/websocket v1.8.13
9 | github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
10 | github.com/google/uuid v1.6.0
11 | github.com/gorilla/mux v1.8.0
12 | github.com/mattn/go-pointer v0.0.1
13 | github.com/rs/zerolog v1.34.0
14 | github.com/stretchr/testify v1.10.0
15 | github.com/tidwall/gjson v1.18.0
16 | go.mau.fi/util v0.8.7
17 | golang.org/x/crypto v0.38.0
18 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
19 | golang.org/x/net v0.40.0
20 | google.golang.org/protobuf v1.36.6
21 | maunium.net/go/mautrix v0.24.1-0.20250531150347-788621f7e035
22 | )
23 |
24 | require (
25 | filippo.io/edwards25519 v1.1.0 // indirect
26 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect
27 | github.com/davecgh/go-spew v1.1.1 // indirect
28 | github.com/gorilla/websocket v1.5.0 // indirect
29 | github.com/kr/pretty v0.3.1 // indirect
30 | github.com/lib/pq v1.10.9 // indirect
31 | github.com/mattn/go-colorable v0.1.14 // indirect
32 | github.com/mattn/go-isatty v0.0.20 // indirect
33 | github.com/mattn/go-sqlite3 v1.14.28 // indirect
34 | github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect
35 | github.com/pmezard/go-difflib v1.0.0 // indirect
36 | github.com/rogpeppe/go-internal v1.10.0 // indirect
37 | github.com/rs/xid v1.6.0 // indirect
38 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
39 | github.com/tidwall/match v1.1.1 // indirect
40 | github.com/tidwall/pretty v1.2.1 // indirect
41 | github.com/tidwall/sjson v1.2.5 // indirect
42 | github.com/yuin/goldmark v1.7.11 // indirect
43 | go.mau.fi/zeroconfig v0.1.3 // indirect
44 | golang.org/x/sync v0.14.0 // indirect
45 | golang.org/x/sys v0.33.0 // indirect
46 | golang.org/x/text v0.25.0 // indirect
47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
48 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
49 | gopkg.in/yaml.v3 v3.0.1 // indirect
50 | maunium.net/go/mauflag v1.0.0 // indirect
51 | )
52 |
--------------------------------------------------------------------------------
/pkg/connector/config.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package connector
18 |
19 | import (
20 | _ "embed"
21 | "strings"
22 | "text/template"
23 |
24 | up "go.mau.fi/util/configupgrade"
25 | "maunium.net/go/mautrix/id"
26 |
27 | "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
28 | )
29 |
30 | //go:embed example-config.yaml
31 | var ExampleConfig string
32 |
33 | type SignalConfig struct {
34 | DisplaynameTemplate string `yaml:"displayname_template"`
35 | UseContactAvatars bool `yaml:"use_contact_avatars"`
36 | SyncContactsOnStartup bool `yaml:"sync_contacts_on_startup"`
37 | UseOutdatedProfiles bool `yaml:"use_outdated_profiles"`
38 | NumberInTopic bool `yaml:"number_in_topic"`
39 | DeviceName string `yaml:"device_name"`
40 | NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
41 | LocationFormat string `yaml:"location_format"`
42 | DisappearViewOnce bool `yaml:"disappear_view_once"`
43 |
44 | displaynameTemplate *template.Template `yaml:"-"`
45 | }
46 |
47 | type DisplaynameParams struct {
48 | ProfileName string
49 | ContactName string
50 | Username string
51 | PhoneNumber string
52 | UUID string
53 | ACI string
54 | PNI string
55 | AboutEmoji string
56 | }
57 |
58 | func (c *SignalConfig) FormatDisplayname(contact *types.Recipient) string {
59 | var nameBuf strings.Builder
60 | err := c.displaynameTemplate.Execute(&nameBuf, &DisplaynameParams{
61 | ProfileName: contact.Profile.Name,
62 | ContactName: contact.ContactName,
63 | Username: "",
64 | PhoneNumber: contact.E164,
65 | UUID: contact.ACI.String(),
66 | ACI: contact.ACI.String(),
67 | PNI: contact.PNI.String(),
68 | AboutEmoji: contact.Profile.AboutEmoji,
69 | })
70 | if err != nil {
71 | panic(err)
72 | }
73 | return nameBuf.String()
74 | }
75 |
76 | func upgradeConfig(helper up.Helper) {
77 | helper.Copy(up.Str, "displayname_template")
78 | helper.Copy(up.Bool, "use_contact_avatars")
79 | helper.Copy(up.Bool, "sync_contacts_on_startup")
80 | helper.Copy(up.Bool, "use_outdated_profiles")
81 | helper.Copy(up.Bool, "number_in_topic")
82 | helper.Copy(up.Str, "device_name")
83 | helper.Copy(up.Str, "note_to_self_avatar")
84 | helper.Copy(up.Str, "location_format")
85 | helper.Copy(up.Bool, "disappear_view_once")
86 | }
87 |
88 | func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {
89 | return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig)
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/connector/dbmeta.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package connector
18 |
19 | import (
20 | "maunium.net/go/mautrix/bridgev2/database"
21 |
22 | "go.mau.fi/mautrix-signal/pkg/signalid"
23 | )
24 |
25 | func (s *SignalConnector) GetDBMetaTypes() database.MetaTypes {
26 | return database.MetaTypes{
27 | Portal: func() any {
28 | return &signalid.PortalMetadata{}
29 | },
30 | Ghost: func() any {
31 | return &signalid.GhostMetadata{}
32 | },
33 | Message: func() any {
34 | return &signalid.MessageMetadata{}
35 | },
36 | Reaction: nil,
37 | UserLogin: func() any {
38 | return &signalid.UserLoginMetadata{}
39 | },
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/connector/example-config.yaml:
--------------------------------------------------------------------------------
1 | # Displayname template for Signal users.
2 | # {{.ProfileName}} - The Signal profile name set by the user.
3 | # {{.ContactName}} - The name for the user from your phone's contact list. This is not safe on multi-user instances.
4 | # {{.PhoneNumber}} - The phone number of the user.
5 | # {{.UUID}} - The UUID of the Signal user.
6 | # {{.AboutEmoji}} - The emoji set by the user in their profile.
7 | displayname_template: '{{or .ProfileName .PhoneNumber "Unknown user"}}'
8 | # Should avatars from the user's contact list be used? This is not safe on multi-user instances.
9 | use_contact_avatars: false
10 | # Should the bridge request the user's contact list from the phone on startup?
11 | sync_contacts_on_startup: true
12 | # Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.
13 | use_outdated_profiles: false
14 | # Should the Signal user's phone number be included in the room topic in private chat portal rooms?
15 | number_in_topic: true
16 | # Default device name that shows up in the Signal app.
17 | device_name: mautrix-signal
18 | # Avatar image for the Note to Self room.
19 | note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
20 | # Format for generating URLs from location messages for sending to Signal.
21 | # Google Maps: 'https://www.google.com/maps/place/%[1]s,%[2]s'
22 | # OpenStreetMap: 'https://www.openstreetmap.org/?mlat=%[1]s&mlon=%[2]s'
23 | location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'
24 | # Should view-once messages disappear shortly after sending a read receipt on Matrix?
25 | disappear_view_once: false
26 |
--------------------------------------------------------------------------------
/pkg/connector/id.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package connector
18 |
19 | import (
20 | "github.com/google/uuid"
21 | "maunium.net/go/mautrix/bridgev2"
22 | "maunium.net/go/mautrix/bridgev2/networkid"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | "go.mau.fi/mautrix-signal/pkg/signalid"
26 | )
27 |
28 | func (s *SignalClient) makePortalKey(chatID string) networkid.PortalKey {
29 | key := networkid.PortalKey{ID: networkid.PortalID(chatID)}
30 | // For non-group chats, add receiver
31 | if s.Main.Bridge.Config.SplitPortals || len(chatID) != 44 {
32 | key.Receiver = s.UserLogin.ID
33 | }
34 | return key
35 | }
36 |
37 | func (s *SignalClient) makeDMPortalKey(serviceID libsignalgo.ServiceID) networkid.PortalKey {
38 | return networkid.PortalKey{
39 | ID: signalid.MakeDMPortalID(serviceID),
40 | Receiver: s.UserLogin.ID,
41 | }
42 | }
43 |
44 | func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
45 | return bridgev2.EventSender{
46 | IsFromMe: sender == s.Client.Store.ACI,
47 | SenderLogin: signalid.MakeUserLoginID(sender),
48 | Sender: signalid.MakeUserID(sender),
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/README.md:
--------------------------------------------------------------------------------
1 | # libsignalgo
2 | Go bindings for [libsignal](https://github.com/signalapp/libsignal).
3 |
4 | ## Installation
5 | 0. Install Rust. You may also need to install libclang-dev and cbindgen manually.
6 | 1. Clone [libsignal](https://github.com/signalapp/libsignal) somewhere.
7 | 2. Run `./update-ffi.sh ` (this builds the library, regenerates the header, and copies them both here)
8 | 3. Copy `libsignal_ffi.a` to `/usr/lib/`.
9 | * Alternatively, set `LIBRARY_PATH` to the directory containing `libsignal_ffi.a`.
10 | Something like this: `LIBRARY_PATH="$LIBRARY_PATH:./pkg/libsignalgo" ./build.sh`
11 | 4. Use like a normal Go library.
12 |
13 | ## Precompiled
14 | You can find precompiled `libsignal_ffi.a`'s on
15 | [mau.dev/tulir/gomuks-build-docker](https://mau.dev/tulir/gomuks-build-docker).
16 | Direct links:
17 | * [Linux amd64](https://mau.dev/tulir/gomuks-build-docker/-/jobs/artifacts/master/raw/libsignal_ffi.a?job=libsignal%20linux%20amd64)
18 | * [Linux arm64](https://mau.dev/tulir/gomuks-build-docker/-/jobs/artifacts/master/raw/libsignal_ffi.a?job=libsignal%20linux%20arm64)
19 | * [macOS arm64](https://mau.dev/tulir/gomuks-build-docker/-/jobs/artifacts/master/raw/libsignal_ffi.a?job=libsignal%20macos%20arm64)
20 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/accountentropy.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "runtime"
25 | "unsafe"
26 | )
27 |
28 | type AccountEntropyPool string
29 |
30 | func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
31 | var out [C.SignalSVR_KEY_LEN]byte
32 | signalFfiError := C.signal_account_entropy_pool_derive_svr_key(
33 | (*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
34 | C.CString(string(aep)),
35 | )
36 | runtime.KeepAlive(aep)
37 | if signalFfiError != nil {
38 | return nil, wrapError(signalFfiError)
39 | }
40 | return out[:], nil
41 | }
42 |
43 | func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) {
44 | var out [C.SignalBACKUP_KEY_LEN]byte
45 | signalFfiError := C.signal_account_entropy_pool_derive_backup_key(
46 | (*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
47 | C.CString(string(aep)),
48 | )
49 | runtime.KeepAlive(aep)
50 | if signalFfiError != nil {
51 | return nil, wrapError(signalFfiError)
52 | }
53 | return out[:], nil
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/address.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import (
25 | "runtime"
26 | )
27 |
28 | type Address struct {
29 | nc noCopy
30 | ptr *C.SignalProtocolAddress
31 | }
32 |
33 | func wrapAddress(ptr *C.SignalProtocolAddress) *Address {
34 | address := &Address{ptr: ptr}
35 | runtime.SetFinalizer(address, (*Address).Destroy)
36 | return address
37 | }
38 |
39 | func NewUUIDAddressFromString(uuidStr string, deviceID uint) (*Address, error) {
40 | serviceID, err := ServiceIDFromString(uuidStr)
41 | if err != nil {
42 | return nil, err
43 | }
44 | return serviceID.Address(deviceID)
45 | }
46 |
47 | func newAddress(name string, deviceID uint) (*Address, error) {
48 | var pa C.SignalMutPointerProtocolAddress
49 | signalFfiError := C.signal_address_new(&pa, C.CString(name), C.uint(deviceID))
50 | if signalFfiError != nil {
51 | return nil, wrapError(signalFfiError)
52 | }
53 | return wrapAddress(pa.raw), nil
54 | }
55 |
56 | func (pa *Address) mutPtr() C.SignalMutPointerProtocolAddress {
57 | return C.SignalMutPointerProtocolAddress{pa.ptr}
58 | }
59 |
60 | func (pa *Address) constPtr() C.SignalConstPointerProtocolAddress {
61 | return C.SignalConstPointerProtocolAddress{pa.ptr}
62 | }
63 |
64 | func (pa *Address) Clone() (*Address, error) {
65 | var cloned C.SignalMutPointerProtocolAddress
66 | signalFfiError := C.signal_address_clone(&cloned, pa.constPtr())
67 | runtime.KeepAlive(pa)
68 | if signalFfiError != nil {
69 | return nil, wrapError(signalFfiError)
70 | }
71 | return wrapAddress(cloned.raw), nil
72 | }
73 |
74 | func (pa *Address) Destroy() error {
75 | pa.CancelFinalizer()
76 | return wrapError(C.signal_address_destroy(pa.mutPtr()))
77 | }
78 |
79 | func (pa *Address) CancelFinalizer() {
80 | runtime.SetFinalizer(pa, nil)
81 | }
82 |
83 | func (pa *Address) Name() (string, error) {
84 | var name *C.char
85 | signalFfiError := C.signal_address_get_name(&name, pa.constPtr())
86 | runtime.KeepAlive(pa)
87 | if signalFfiError != nil {
88 | return "", wrapError(signalFfiError)
89 | }
90 | return CopyCStringToString(name), nil
91 | }
92 |
93 | func (pa *Address) NameServiceID() (ServiceID, error) {
94 | name, err := pa.Name()
95 | if err != nil {
96 | return ServiceID{}, err
97 | }
98 | return ServiceIDFromString(name)
99 | }
100 |
101 | func (pa *Address) DeviceID() (uint, error) {
102 | var deviceID C.uint
103 | signalFfiError := C.signal_address_get_device_id(&deviceID, pa.constPtr())
104 | runtime.KeepAlive(pa)
105 | if signalFfiError != nil {
106 | return 0, wrapError(signalFfiError)
107 | }
108 | return uint(deviceID), nil
109 | }
110 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/address_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/google/uuid"
23 | "github.com/stretchr/testify/assert"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
26 | )
27 |
28 | // From PublicAPITests.swift:testAddress
29 | func TestAddress(t *testing.T) {
30 | setupLogging()
31 |
32 | testUUID := uuid.New()
33 |
34 | addr, err := libsignalgo.NewPNIServiceID(testUUID).Address(5)
35 | assert.NoError(t, err)
36 |
37 | name, err := addr.Name()
38 | assert.NoError(t, err)
39 | assert.Equal(t, "PNI:"+testUUID.String(), name)
40 |
41 | deviceID, err := addr.DeviceID()
42 | assert.NoError(t, err)
43 | assert.Equal(t, uint(5), deviceID)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/aes256gcmsiv.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import "runtime"
25 |
26 | type AES256_GCM_SIV struct {
27 | nc noCopy
28 | ptr *C.SignalAes256GcmSiv
29 | }
30 |
31 | func wrapAES256_GCM_SIV(ptr *C.SignalAes256GcmSiv) *AES256_GCM_SIV {
32 | aes := &AES256_GCM_SIV{ptr: ptr}
33 | runtime.SetFinalizer(aes, (*AES256_GCM_SIV).Destroy)
34 | return aes
35 | }
36 |
37 | func NewAES256_GCM_SIV(key []byte) (*AES256_GCM_SIV, error) {
38 | var aes C.SignalMutPointerAes256GcmSiv
39 | signalFfiError := C.signal_aes256_gcm_siv_new(&aes, BytesToBuffer(key))
40 | if signalFfiError != nil {
41 | return nil, wrapError(signalFfiError)
42 | }
43 | return wrapAES256_GCM_SIV(aes.raw), nil
44 | }
45 |
46 | func (aes *AES256_GCM_SIV) mutPtr() C.SignalMutPointerAes256GcmSiv {
47 | return C.SignalMutPointerAes256GcmSiv{aes.ptr}
48 | }
49 |
50 | func (aes *AES256_GCM_SIV) constPtr() C.SignalConstPointerAes256GcmSiv {
51 | return C.SignalConstPointerAes256GcmSiv{aes.ptr}
52 | }
53 |
54 | func (aes *AES256_GCM_SIV) Destroy() error {
55 | runtime.SetFinalizer(aes, nil)
56 | return wrapError(C.signal_aes256_gcm_siv_destroy(C.SignalMutPointerAes256GcmSiv{raw: aes.ptr}))
57 | }
58 |
59 | func (aes *AES256_GCM_SIV) Encrypt(plaintext, nonce, associatedData []byte) ([]byte, error) {
60 | var encrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
61 |
62 | signalFfiError := C.signal_aes256_gcm_siv_encrypt(
63 | &encrypted,
64 | C.SignalConstPointerAes256GcmSiv{raw: aes.ptr},
65 | BytesToBuffer(plaintext),
66 | BytesToBuffer(nonce),
67 | BytesToBuffer(associatedData),
68 | )
69 | runtime.KeepAlive(aes)
70 | if signalFfiError != nil {
71 | return nil, wrapError(signalFfiError)
72 | }
73 | return CopySignalOwnedBufferToBytes(encrypted), nil
74 | }
75 |
76 | func (aes *AES256_GCM_SIV) Decrypt(ciphertext, nonce, associatedData []byte) ([]byte, error) {
77 | var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
78 | signalFfiError := C.signal_aes256_gcm_siv_decrypt(
79 | &decrypted,
80 | C.SignalConstPointerAes256GcmSiv{raw: aes.ptr},
81 | BytesToBuffer(ciphertext),
82 | BytesToBuffer(nonce),
83 | BytesToBuffer(associatedData),
84 | )
85 | if signalFfiError != nil {
86 | return nil, wrapError(signalFfiError)
87 | }
88 | return CopySignalOwnedBufferToBytes(decrypted), nil
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/aes256gcmsiv_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | // From PublicAPITests.swift:testAesGcmSiv
28 | func TestAES256_GCM_SIV(t *testing.T) {
29 | plaintext := []byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
30 | expectedCiphertext := []byte{0x1d, 0xe2, 0x29, 0x67, 0x23, 0x7a, 0x81, 0x32, 0x91, 0x21, 0x3f, 0x26, 0x7e, 0x3b, 0x45, 0x2f, 0x02, 0xd0, 0x1a, 0xe3, 0x3e, 0x4e, 0xc8, 0x54}
31 | associatedData := []byte{0x01}
32 | key := []byte{
33 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
34 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
35 | }
36 | nonce := []byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
37 |
38 | gcmSiv, err := libsignalgo.NewAES256_GCM_SIV(key)
39 | assert.NoError(t, err)
40 |
41 | ciphertext, err := gcmSiv.Encrypt(plaintext, nonce, associatedData)
42 | assert.NoError(t, err)
43 | assert.Equal(t, expectedCiphertext, ciphertext)
44 |
45 | recovered, err := gcmSiv.Decrypt(ciphertext, nonce, associatedData)
46 | assert.NoError(t, err)
47 | assert.Equal(t, plaintext, recovered)
48 |
49 | _, err = gcmSiv.Decrypt(plaintext, nonce, associatedData)
50 | assert.Error(t, err)
51 | _, err = gcmSiv.Decrypt(ciphertext, associatedData, nonce)
52 | assert.Error(t, err)
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/buffer.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "unsafe"
25 | )
26 |
27 | func BorrowedMutableBuffer(length int) C.SignalBorrowedMutableBuffer {
28 | data := make([]byte, length)
29 | return C.SignalBorrowedMutableBuffer{
30 | base: (*C.uchar)(unsafe.Pointer(&data[0])),
31 | length: C.size_t(len(data)),
32 | }
33 | }
34 |
35 | func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
36 | buf := C.SignalBorrowedBuffer{
37 | length: C.size_t(len(data)),
38 | }
39 | if len(data) > 0 {
40 | buf.base = (*C.uchar)(unsafe.Pointer(&data[0]))
41 | }
42 | return buf
43 | }
44 |
45 | func EmptyBorrowedBuffer() C.SignalBorrowedBuffer {
46 | return C.SignalBorrowedBuffer{}
47 | }
48 |
49 | // TODO: Try out this code from ChatGPT that might be more memory safe
50 | // - Makes copy of data
51 | // - Sets finalizer to free memory
52 | //
53 | //type CBytesWrapper struct {
54 | // c unsafe.Pointer
55 | //}
56 | //
57 | //func CBytes(b []byte) *CBytesWrapper {
58 | // if len(b) == 0 {
59 | // return &CBytesWrapper{nil}
60 | // }
61 | // c := C.malloc(C.size_t(len(b)))
62 | // copy((*[1 << 30]byte)(c)[:], b)
63 | // return &CBytesWrapper{c}
64 | //}
65 | //
66 | //func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
67 | // cData := CBytes(data)
68 | // buf := C.SignalBorrowedBuffer{
69 | // length: C.uintptr_t(len(data)),
70 | // }
71 | // if len(data) > 0 {
72 | // buf.base = (*C.uchar)(cData.c)
73 | // }
74 | //
75 | // // Setting finalizer here
76 | // runtime.SetFinalizer(cData, func(c *CBytesWrapper) { C.free(c.c) })
77 | //
78 | // return buf
79 | //}
80 | //
81 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/cflags.go:
--------------------------------------------------------------------------------
1 | package libsignalgo
2 |
3 | /*
4 | #cgo LDFLAGS: -lsignal_ffi -ldl -lm
5 | */
6 | import "C"
7 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/ciphertextmessage.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import "runtime"
25 |
26 | type CiphertextMessageType uint8
27 |
28 | const (
29 | CiphertextMessageTypeWhisper CiphertextMessageType = 2
30 | CiphertextMessageTypePreKey CiphertextMessageType = 3
31 | CiphertextMessageTypeSenderKey CiphertextMessageType = 7
32 | CiphertextMessageTypePlaintext CiphertextMessageType = 8
33 | )
34 |
35 | type CiphertextMessage struct {
36 | nc noCopy
37 | ptr *C.SignalCiphertextMessage
38 | }
39 |
40 | func wrapCiphertextMessage(ptr *C.SignalCiphertextMessage) *CiphertextMessage {
41 | ciphertextMessage := &CiphertextMessage{ptr: ptr}
42 | runtime.SetFinalizer(ciphertextMessage, (*CiphertextMessage).Destroy)
43 | return ciphertextMessage
44 | }
45 |
46 | func NewCiphertextMessage(plaintext *PlaintextContent) (*CiphertextMessage, error) {
47 | var ciphertextMessage C.SignalMutPointerCiphertextMessage
48 | signalFfiError := C.signal_ciphertext_message_from_plaintext_content(
49 | &ciphertextMessage,
50 | plaintext.constPtr(),
51 | )
52 | if signalFfiError != nil {
53 | return nil, wrapError(signalFfiError)
54 | }
55 | return wrapCiphertextMessage(ciphertextMessage.raw), nil
56 | }
57 |
58 | func (c *CiphertextMessage) mutPtr() C.SignalMutPointerCiphertextMessage {
59 | return C.SignalMutPointerCiphertextMessage{c.ptr}
60 | }
61 |
62 | func (c *CiphertextMessage) constPtr() C.SignalConstPointerCiphertextMessage {
63 | return C.SignalConstPointerCiphertextMessage{c.ptr}
64 | }
65 |
66 | func (c *CiphertextMessage) Destroy() error {
67 | c.CancelFinalizer()
68 | return wrapError(C.signal_ciphertext_message_destroy(c.mutPtr()))
69 | }
70 |
71 | func (c *CiphertextMessage) CancelFinalizer() {
72 | runtime.SetFinalizer(c, nil)
73 | }
74 |
75 | func (c *CiphertextMessage) Serialize() ([]byte, error) {
76 | var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
77 | signalFfiError := C.signal_ciphertext_message_serialize(&serialized, c.constPtr())
78 | runtime.KeepAlive(c)
79 | if signalFfiError != nil {
80 | return nil, wrapError(signalFfiError)
81 | }
82 | return CopySignalOwnedBufferToBytes(serialized), nil
83 | }
84 |
85 | func (c *CiphertextMessage) MessageType() (CiphertextMessageType, error) {
86 | var messageType C.uint8_t
87 | signalFfiError := C.signal_ciphertext_message_type(&messageType, c.constPtr())
88 | runtime.KeepAlive(c)
89 | if signalFfiError != nil {
90 | return 0, wrapError(signalFfiError)
91 | }
92 | return CiphertextMessageType(messageType), nil
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/conversions.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import "unsafe"
24 |
25 | func CopyCStringToString(cString *C.char) (s string) {
26 | s = C.GoString(cString)
27 | C.signal_free_string(cString)
28 | return
29 | }
30 |
31 | func CopyBufferToBytes(buffer *C.uchar, length C.size_t) (b []byte) {
32 | b = C.GoBytes(unsafe.Pointer(buffer), C.int(length))
33 | C.signal_free_buffer(buffer, length)
34 | return
35 | }
36 |
37 | func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
38 | b = C.GoBytes(unsafe.Pointer(buffer.base), C.int(buffer.length))
39 | C.signal_free_buffer(buffer.base, buffer.length)
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/devicetransfer.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "runtime"
25 | )
26 |
27 | type DeviceTransferKey struct {
28 | privateKey []byte
29 | }
30 |
31 | func GenerateDeviceTransferKey() (*DeviceTransferKey, error) {
32 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
33 | signalFfiError := C.signal_device_transfer_generate_private_key(&resp)
34 | if signalFfiError != nil {
35 | return nil, wrapError(signalFfiError)
36 | }
37 | return &DeviceTransferKey{privateKey: CopySignalOwnedBufferToBytes(resp)}, nil
38 | }
39 |
40 | func (dtk *DeviceTransferKey) PrivateKeyMaterial() []byte {
41 | return dtk.privateKey
42 | }
43 |
44 | func (dtk *DeviceTransferKey) GenerateCertificate(name string, days int) ([]byte, error) {
45 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
46 | signalFfiError := C.signal_device_transfer_generate_certificate(&resp, BytesToBuffer(dtk.privateKey), C.CString(name), C.uint32_t(days))
47 | runtime.KeepAlive(dtk)
48 | if signalFfiError != nil {
49 | return nil, wrapError(signalFfiError)
50 | }
51 | return CopySignalOwnedBufferToBytes(resp), nil
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/devicetransfer_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | // From PublicAPITests.swift:testDeviceTransferKey
28 | func TestDeviceTransferKey(t *testing.T) {
29 | deviceKey, err := libsignalgo.GenerateDeviceTransferKey()
30 | assert.NoError(t, err)
31 |
32 | /*
33 | Anything encoded in an ASN.1 SEQUENCE starts with 0x30 when encoded
34 | as DER. (This test could be better.)
35 | */
36 | key := deviceKey.PrivateKeyMaterial()
37 | assert.Greater(t, len(key), 0)
38 | assert.EqualValues(t, 0x30, key[0])
39 |
40 | cert, err := deviceKey.GenerateCertificate("name", 30)
41 | assert.NoError(t, err)
42 | assert.Greater(t, len(cert), 0)
43 | assert.EqualValues(t, 0x30, cert[0])
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/error.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "fmt"
25 | )
26 |
27 | type ErrorCode int
28 |
29 | const (
30 | ErrorCodeUnknownError ErrorCode = 1
31 | ErrorCodeInvalidState ErrorCode = 2
32 | ErrorCodeInternalError ErrorCode = 3
33 | ErrorCodeNullParameter ErrorCode = 4
34 | ErrorCodeInvalidArgument ErrorCode = 5
35 | ErrorCodeInvalidType ErrorCode = 6
36 | ErrorCodeInvalidUtf8String ErrorCode = 7
37 | ErrorCodeProtobufError ErrorCode = 10
38 | ErrorCodeLegacyCiphertextVersion ErrorCode = 21
39 | ErrorCodeUnknownCiphertextVersion ErrorCode = 22
40 | ErrorCodeUnrecognizedMessageVersion ErrorCode = 23
41 | ErrorCodeInvalidMessage ErrorCode = 30
42 | ErrorCodeSealedSenderSelfSend ErrorCode = 31
43 | ErrorCodeInvalidKey ErrorCode = 40
44 | ErrorCodeInvalidSignature ErrorCode = 41
45 | ErrorCodeInvalidAttestationData ErrorCode = 42
46 | ErrorCodeFingerprintVersionMismatch ErrorCode = 51
47 | ErrorCodeFingerprintParsingError ErrorCode = 52
48 | ErrorCodeUntrustedIdentity ErrorCode = 60
49 | ErrorCodeInvalidKeyIdentifier ErrorCode = 70
50 | ErrorCodeSessionNotFound ErrorCode = 80
51 | ErrorCodeInvalidRegistrationId ErrorCode = 81
52 | ErrorCodeInvalidSession ErrorCode = 82
53 | ErrorCodeInvalidSenderKeySession ErrorCode = 83
54 | ErrorCodeDuplicatedMessage ErrorCode = 90
55 | ErrorCodeCallbackError ErrorCode = 100
56 | ErrorCodeVerificationFailure ErrorCode = 110
57 | )
58 |
59 | type SignalError struct {
60 | Code ErrorCode
61 | Message string
62 | }
63 |
64 | func (e *SignalError) Error() string {
65 | return fmt.Sprintf("%d: %s", e.Code, e.Message)
66 | }
67 |
68 | func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error {
69 | if signalError == nil {
70 | return nil
71 | }
72 |
73 | defer C.signal_error_free(signalError)
74 |
75 | errorType := C.signal_error_get_type(signalError)
76 | if ErrorCode(errorType) == ErrorCodeCallbackError {
77 | return ctx.Error
78 | } else {
79 | return wrapSignalError(signalError, errorType)
80 | }
81 | }
82 |
83 | func wrapError(signalError *C.SignalFfiError) error {
84 | if signalError == nil {
85 | return nil
86 | }
87 |
88 | defer C.signal_error_free(signalError)
89 |
90 | return wrapSignalError(signalError, C.signal_error_get_type(signalError))
91 | }
92 |
93 | func wrapSignalError(signalError *C.SignalFfiError, errorType C.uint32_t) error {
94 | var messageBytes *C.char
95 | getMessageError := C.signal_error_get_message(signalError, &messageBytes)
96 | if getMessageError != nil {
97 | // Ignore any errors from this, it will just end up being an empty string.
98 | C.signal_error_free(getMessageError)
99 | }
100 | return &SignalError{Code: ErrorCode(errorType), Message: CopyCStringToString(messageBytes)}
101 | }
102 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/groupcipher.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import (
25 | "context"
26 | "runtime"
27 | "unsafe"
28 |
29 | "github.com/google/uuid"
30 | )
31 |
32 | func GroupEncrypt(ctx context.Context, ptext []byte, sender *Address, distributionID uuid.UUID, store SenderKeyStore) (*CiphertextMessage, error) {
33 | callbackCtx := NewCallbackContext(ctx)
34 | defer callbackCtx.Unref()
35 | var ciphertextMessage C.SignalMutPointerCiphertextMessage
36 | signalFfiError := C.signal_group_encrypt_message(
37 | &ciphertextMessage,
38 | sender.constPtr(),
39 | (*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
40 | BytesToBuffer(ptext),
41 | callbackCtx.wrapSenderKeyStore(store))
42 | runtime.KeepAlive(ptext)
43 | runtime.KeepAlive(sender)
44 | if signalFfiError != nil {
45 | return nil, callbackCtx.wrapError(signalFfiError)
46 | }
47 | return wrapCiphertextMessage(ciphertextMessage.raw), nil
48 | }
49 |
50 | func GroupDecrypt(ctx context.Context, ctext []byte, sender *Address, store SenderKeyStore) ([]byte, error) {
51 | callbackCtx := NewCallbackContext(ctx)
52 | defer callbackCtx.Unref()
53 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
54 | signalFfiError := C.signal_group_decrypt_message(
55 | &resp,
56 | sender.constPtr(),
57 | BytesToBuffer(ctext),
58 | callbackCtx.wrapSenderKeyStore(store))
59 | runtime.KeepAlive(ctext)
60 | runtime.KeepAlive(sender)
61 | if signalFfiError != nil {
62 | return nil, callbackCtx.wrapError(signalFfiError)
63 | }
64 | return CopySignalOwnedBufferToBytes(resp), nil
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/groupcipher_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "context"
21 | "testing"
22 |
23 | "github.com/google/uuid"
24 | "github.com/stretchr/testify/assert"
25 |
26 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
27 | )
28 |
29 | // From PublicAPITests.swift:testGroupCipher
30 | func TestGroupCipher(t *testing.T) {
31 | ctx := context.TODO()
32 |
33 | sender, err := libsignalgo.NewACIServiceID(uuid.New()).Address(4)
34 | assert.NoError(t, err)
35 |
36 | distributionID, err := uuid.Parse("d1d1d1d1-7000-11eb-b32a-33b8a8a487a6")
37 | assert.NoError(t, err)
38 |
39 | aliceStore := NewInMemorySignalProtocolStore()
40 |
41 | skdm, err := libsignalgo.NewSenderKeyDistributionMessage(ctx, sender, distributionID, aliceStore)
42 | assert.NoError(t, err)
43 |
44 | serialized, err := skdm.Serialize()
45 | assert.NoError(t, err)
46 |
47 | skdmReloaded, err := libsignalgo.DeserializeSenderKeyDistributionMessage(serialized)
48 | assert.NoError(t, err)
49 |
50 | aliceCiphertextMessage, err := libsignalgo.GroupEncrypt(ctx, []byte{1, 2, 3}, sender, distributionID, aliceStore)
51 | assert.NoError(t, err)
52 |
53 | aliceCiphertext, err := aliceCiphertextMessage.Serialize()
54 | assert.NoError(t, err)
55 |
56 | bobStore := NewInMemorySignalProtocolStore()
57 | err = skdmReloaded.Process(ctx, sender, bobStore)
58 | assert.NoError(t, err)
59 |
60 | bobPtext, err := libsignalgo.GroupDecrypt(ctx, aliceCiphertext, sender, bobStore)
61 | assert.NoError(t, err)
62 | assert.Equal(t, []byte{1, 2, 3}, bobPtext)
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/hsmenclave.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import "runtime"
25 |
26 | type HSMEnclaveClient struct {
27 | nc noCopy
28 | ptr *C.SignalHsmEnclaveClient
29 | }
30 |
31 | func wrapHSMEnclaveClient(ptr *C.SignalHsmEnclaveClient) *HSMEnclaveClient {
32 | hsmEnclaveClient := &HSMEnclaveClient{ptr: ptr}
33 | runtime.SetFinalizer(hsmEnclaveClient, (*HSMEnclaveClient).Destroy)
34 | return hsmEnclaveClient
35 | }
36 |
37 | func NewHSMEnclaveClient(trustedPublicKey, trustedCodeHashes []byte) (*HSMEnclaveClient, error) {
38 | var cds C.SignalMutPointerHsmEnclaveClient
39 | signalFfiError := C.signal_hsm_enclave_client_new(
40 | &cds,
41 | BytesToBuffer(trustedPublicKey),
42 | BytesToBuffer(trustedCodeHashes),
43 | )
44 | if signalFfiError != nil {
45 | return nil, wrapError(signalFfiError)
46 | }
47 | return wrapHSMEnclaveClient(cds.raw), nil
48 | }
49 |
50 | func (hsm *HSMEnclaveClient) mutPtr() C.SignalMutPointerHsmEnclaveClient {
51 | return C.SignalMutPointerHsmEnclaveClient{hsm.ptr}
52 | }
53 |
54 | func (hsm *HSMEnclaveClient) constPtr() C.SignalConstPointerHsmEnclaveClient {
55 | return C.SignalConstPointerHsmEnclaveClient{hsm.ptr}
56 | }
57 |
58 | func (hsm *HSMEnclaveClient) Destroy() error {
59 | runtime.SetFinalizer(hsm, nil)
60 | return wrapError(C.signal_hsm_enclave_client_destroy(hsm.mutPtr()))
61 | }
62 |
63 | func (hsm *HSMEnclaveClient) InitialRequest() ([]byte, error) {
64 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
65 | signalFfiError := C.signal_hsm_enclave_client_initial_request(&resp, hsm.constPtr())
66 | runtime.KeepAlive(hsm)
67 | if signalFfiError != nil {
68 | return nil, wrapError(signalFfiError)
69 | }
70 | return CopySignalOwnedBufferToBytes(resp), nil
71 | }
72 |
73 | func (hsm *HSMEnclaveClient) CompleteHandshake(handshakeReceived []byte) error {
74 | signalFfiError := C.signal_hsm_enclave_client_complete_handshake(hsm.mutPtr(), BytesToBuffer(handshakeReceived))
75 | runtime.KeepAlive(hsm)
76 | runtime.KeepAlive(handshakeReceived)
77 | return wrapError(signalFfiError)
78 | }
79 |
80 | func (hsm *HSMEnclaveClient) EstablishedSend(plaintext []byte) ([]byte, error) {
81 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
82 | signalFfiError := C.signal_hsm_enclave_client_established_send(&resp, hsm.mutPtr(), BytesToBuffer(plaintext))
83 | runtime.KeepAlive(hsm)
84 | runtime.KeepAlive(plaintext)
85 | if signalFfiError != nil {
86 | return nil, wrapError(signalFfiError)
87 | }
88 | return CopySignalOwnedBufferToBytes(resp), nil
89 | }
90 |
91 | func (hsm *HSMEnclaveClient) EstablishedReceive(ciphertext []byte) ([]byte, error) {
92 | var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
93 | signalFfiError := C.signal_hsm_enclave_client_established_recv(&resp, hsm.mutPtr(), BytesToBuffer(ciphertext))
94 | runtime.KeepAlive(hsm)
95 | runtime.KeepAlive(ciphertext)
96 | if signalFfiError != nil {
97 | return nil, wrapError(signalFfiError)
98 | }
99 | return CopySignalOwnedBufferToBytes(resp), nil
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/hsmenclave_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | var nullHash = []byte{
28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30 | }
31 |
32 | func getKeyBytes(t *testing.T) []byte {
33 | validKey, err := libsignalgo.GenerateIdentityKeyPair()
34 | assert.NoError(t, err)
35 | keyBytes, err := validKey.GetPublicKey().Bytes()
36 | assert.NoError(t, err)
37 | return keyBytes
38 | }
39 |
40 | // From HsmEnclaveTests.swift:testCreateClient
41 | // From HsmEnclaveTests.swift:testCreateClientFailsWithNoHashes
42 | func TestCreateHSMClient(t *testing.T) {
43 | setupLogging()
44 | hashes := []byte{
45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
47 | //
48 | 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
49 | 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
50 | }
51 | t.Run("Succeeds with hashes", func(t *testing.T) {
52 | client, err := libsignalgo.NewHSMEnclaveClient(getKeyBytes(t), hashes)
53 | assert.NoError(t, err)
54 |
55 | initialMessage, err := client.InitialRequest()
56 | assert.NoError(t, err)
57 | assert.Len(t, initialMessage, 112)
58 | })
59 |
60 | t.Run("Fails with no hashes", func(t *testing.T) {
61 | _, err := libsignalgo.NewHSMEnclaveClient(getKeyBytes(t), []byte{})
62 | assert.Error(t, err)
63 | })
64 | }
65 |
66 | // From HsmEnclaveTests.swift:testCompleteHandshakeWithoutInitialRequest
67 | func TestHSMCompleteHandshakeWithoutInitialRequest(t *testing.T) {
68 | setupLogging()
69 | client, err := libsignalgo.NewHSMEnclaveClient(getKeyBytes(t), nullHash)
70 | assert.NoError(t, err)
71 | err = client.CompleteHandshake([]byte{0x01, 0x02, 0x03})
72 | assert.Error(t, err)
73 | }
74 |
75 | // From HsmEnclaveTests.swift:testEstablishedSendFailsPriorToEstablishment
76 | func TestHSMEstablishedSendFailsPriorToEstablishment(t *testing.T) {
77 | setupLogging()
78 | client, err := libsignalgo.NewHSMEnclaveClient(getKeyBytes(t), nullHash)
79 | assert.NoError(t, err)
80 | _, err = client.EstablishedSend([]byte{0x01, 0x02, 0x03})
81 | assert.Error(t, err)
82 | }
83 |
84 | // From HsmEnclaveTests.swift:testEstablishedRecvFailsPriorToEstablishment
85 | func TestHSMEstablishedReceiveFailsPriorToEstablishment(t *testing.T) {
86 | setupLogging()
87 | client, err := libsignalgo.NewHSMEnclaveClient(getKeyBytes(t), nullHash)
88 | assert.NoError(t, err)
89 | _, err = client.EstablishedReceive([]byte{0x01, 0x02, 0x03})
90 | assert.Error(t, err)
91 | }
92 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/identitykey_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | // From PublicAPITests.swift:testSignAlternateIdentity
28 | func TestSignAlternateIdentity(t *testing.T) {
29 | primary, err := libsignalgo.GenerateIdentityKeyPair()
30 | assert.NoError(t, err)
31 | secondary, err := libsignalgo.GenerateIdentityKeyPair()
32 | assert.NoError(t, err)
33 |
34 | signature, err := secondary.SignAlternateIdentity(primary.GetIdentityKey())
35 | assert.NoError(t, err)
36 |
37 | verified, err := secondary.GetIdentityKey().VerifyAlternateIdentity(primary.GetIdentityKey(), signature)
38 | assert.NoError(t, err)
39 | assert.True(t, verified)
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/kdf.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "runtime"
25 | "unsafe"
26 | )
27 |
28 | func HKDFDerive(outputLength int, inputKeyMaterial, salt, info []byte) ([]byte, error) {
29 | output := BorrowedMutableBuffer(outputLength)
30 | signalFfiError := C.signal_hkdf_derive(output, BytesToBuffer(inputKeyMaterial), BytesToBuffer(info), BytesToBuffer(salt))
31 | runtime.KeepAlive(inputKeyMaterial)
32 | runtime.KeepAlive(salt)
33 | runtime.KeepAlive(info)
34 | if signalFfiError != nil {
35 | return nil, wrapError(signalFfiError)
36 | }
37 | // No need to wrap this in a CopyBufferToBytes since this is allocated by
38 | // Go and thus will be properly garbage collected.
39 | return C.GoBytes(unsafe.Pointer(output.base), C.int(output.length)), nil
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/kdf_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 |
22 | "github.com/stretchr/testify/assert"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | // From PublicAPITests.swift:testHkdfSimple
28 | func TestHKDF_Simple(t *testing.T) {
29 | setupLogging()
30 | inputKeyMaterial := []byte{
31 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
32 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
33 | }
34 | outputKeyMaterial := []byte{0x8d, 0xa4, 0xe7, 0x75}
35 |
36 | derived, err := libsignalgo.HKDFDerive(len(outputKeyMaterial), inputKeyMaterial, []byte{}, []byte{})
37 | assert.NoError(t, err)
38 | assert.Equal(t, outputKeyMaterial, derived)
39 | }
40 |
41 | // From PublicAPITests.swift:testHkdfUsingRFCExample
42 | func TestHKDF_RFCExample(t *testing.T) {
43 | setupLogging()
44 |
45 | var inputKeyMaterial, salt, info []byte
46 | var i byte
47 | for i = 0; i <= 0x4f; i++ {
48 | inputKeyMaterial = append(inputKeyMaterial, i)
49 | }
50 | for i = 0x60; i <= 0xaf; i++ {
51 | salt = append(salt, i)
52 | }
53 | for i = 0xb0; i < 0xff; i++ {
54 | info = append(info, i)
55 | }
56 | info = append(info, 0xff)
57 |
58 | outputKeyMaterial := []byte{
59 | 0xb1, 0x1e, 0x39, 0x8d, 0xc8, 0x03, 0x27, 0xa1, 0xc8, 0xe7, 0xf7, 0x8c, 0x59, 0x6a, 0x49, 0x34,
60 | 0x4f, 0x01, 0x2e, 0xda, 0x2d, 0x4e, 0xfa, 0xd8, 0xa0, 0x50, 0xcc, 0x4c, 0x19, 0xaf, 0xa9, 0x7c,
61 | 0x59, 0x04, 0x5a, 0x99, 0xca, 0xc7, 0x82, 0x72, 0x71, 0xcb, 0x41, 0xc6, 0x5e, 0x59, 0x0e, 0x09,
62 | 0xda, 0x32, 0x75, 0x60, 0x0c, 0x2f, 0x09, 0xb8, 0x36, 0x77, 0x93, 0xa9, 0xac, 0xa3, 0xdb, 0x71,
63 | 0xcc, 0x30, 0xc5, 0x81, 0x79, 0xec, 0x3e, 0x87, 0xc1, 0x4c, 0x01, 0xd5, 0xc1, 0xf3, 0x43, 0x4f,
64 | 0x1d, 0x87,
65 | }
66 |
67 | derived, err := libsignalgo.HKDFDerive(len(outputKeyMaterial), inputKeyMaterial, salt, info)
68 | assert.NoError(t, err)
69 | assert.Equal(t, outputKeyMaterial, derived)
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/kyberprekeystore.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 |
23 | typedef const SignalKyberPreKeyRecord const_kyber_pre_key_record;
24 |
25 | extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalKyberPreKeyRecord **recordp, uint32_t id);
26 | extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, const_kyber_pre_key_record *record);
27 | extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id);
28 | */
29 | import "C"
30 | import (
31 | "context"
32 | "unsafe"
33 | )
34 |
35 | type KyberPreKeyStore interface {
36 | LoadKyberPreKey(ctx context.Context, id uint32) (*KyberPreKeyRecord, error)
37 | StoreKyberPreKey(ctx context.Context, id uint32, kyberPreKeyRecord *KyberPreKeyRecord) error
38 | MarkKyberPreKeyUsed(ctx context.Context, id uint32) error
39 | }
40 |
41 | //export signal_load_kyber_pre_key_callback
42 | func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalKyberPreKeyRecord, id C.uint32_t) C.int {
43 | return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
44 | key, err := store.LoadKyberPreKey(ctx, uint32(id))
45 | if err == nil && key != nil {
46 | key.CancelFinalizer()
47 | *keyp = key.ptr
48 | }
49 | return err
50 | })
51 | }
52 |
53 | //export signal_store_kyber_pre_key_callback
54 | func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_kyber_pre_key_record) C.int {
55 | return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
56 | record := KyberPreKeyRecord{ptr: (*C.SignalKyberPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
57 | cloned, err := record.Clone()
58 | if err != nil {
59 | return err
60 | }
61 | return store.StoreKyberPreKey(ctx, uint32(id), cloned)
62 | })
63 | }
64 |
65 | //export signal_mark_kyber_pre_key_used_callback
66 | func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.int {
67 | return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
68 | err := store.MarkKyberPreKeyUsed(ctx, uint32(id))
69 | return err
70 | })
71 | }
72 |
73 | func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
74 | return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
75 | ctx: wrapStore(ctx, store),
76 | load_kyber_pre_key: C.SignalLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
77 | store_kyber_pre_key: C.SignalStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
78 | mark_kyber_pre_key_used: C.SignalMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
79 | }}
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/logging.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include <./libsignal-ffi.h>
21 |
22 | extern void signal_log_callback(void *ctx, SignalLogLevel level, char *file, uint32_t line, char *message);
23 | extern void signal_log_flush_callback(void *ctx);
24 | */
25 | import "C"
26 | import (
27 | "unsafe"
28 | )
29 |
30 | // ffiLogger is the global logger object.
31 | var ffiLogger Logger
32 |
33 | //export signal_log_callback
34 | func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file *C.char, line C.uint32_t, message *C.char) {
35 | ffiLogger.Log(LogLevel(int(level)), C.GoString(file), uint(line), C.GoString(message))
36 | }
37 |
38 | //export signal_log_flush_callback
39 | func signal_log_flush_callback(ctx unsafe.Pointer) {
40 | ffiLogger.Flush()
41 | }
42 |
43 | type LogLevel int
44 |
45 | const (
46 | LogLevelError LogLevel = iota + 1
47 | LogLevelWarn
48 | LogLevelInfo
49 | LogLevelDebug
50 | LogLevelTrace
51 | )
52 |
53 | type Logger interface {
54 | Log(level LogLevel, file string, line uint, message string)
55 | Flush()
56 | }
57 |
58 | func InitLogger(level LogLevel, logger Logger) {
59 | ffiLogger = logger
60 | C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLogger{
61 | log: C.SignalLogCallback(C.signal_log_callback),
62 | flush: C.SignalLogFlushCallback(C.signal_log_flush_callback),
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/messagebackupkey.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2025 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "runtime"
25 | "unsafe"
26 | )
27 |
28 | type MessageBackupKey struct {
29 | nc noCopy
30 | ptr *C.SignalMessageBackupKey
31 | }
32 |
33 | func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
34 | backupKey := &MessageBackupKey{ptr: ptr}
35 | runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy)
36 | return backupKey
37 | }
38 |
39 | func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceID) (*MessageBackupKey, error) {
40 | var bk C.SignalMutPointerMessageBackupKey
41 | signalFfiError := C.signal_message_backup_key_from_account_entropy_pool(
42 | &bk,
43 | C.CString(string(aep)),
44 | aci.CFixedBytes(),
45 | )
46 | runtime.KeepAlive(aep)
47 | if signalFfiError != nil {
48 | return nil, wrapError(signalFfiError)
49 | }
50 | return wrapMessageBackupKey(bk.raw), nil
51 | }
52 |
53 | func MessageBackupKeyFromBackupKeyAndID(backupKey *BackupKey, backupID *BackupID) (*MessageBackupKey, error) {
54 | var bk C.SignalMutPointerMessageBackupKey
55 | signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id(
56 | &bk,
57 | (*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(backupKey)),
58 | (*[BackupIDLength]C.uint8_t)(unsafe.Pointer(backupID)),
59 | )
60 | runtime.KeepAlive(backupKey)
61 | runtime.KeepAlive(backupID)
62 | if signalFfiError != nil {
63 | return nil, wrapError(signalFfiError)
64 | }
65 | return wrapMessageBackupKey(bk.raw), nil
66 | }
67 |
68 | func (bk *MessageBackupKey) mutPtr() C.SignalMutPointerMessageBackupKey {
69 | return C.SignalMutPointerMessageBackupKey{bk.ptr}
70 | }
71 |
72 | func (bk *MessageBackupKey) constPtr() C.SignalConstPointerMessageBackupKey {
73 | return C.SignalConstPointerMessageBackupKey{bk.ptr}
74 | }
75 |
76 | func (bk *MessageBackupKey) Destroy() error {
77 | runtime.SetFinalizer(bk, nil)
78 | return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr()))
79 | }
80 |
81 | func (bk *MessageBackupKey) GetHMACKey() ([32]byte, error) {
82 | var out [32]byte
83 | signalFfiError := C.signal_message_backup_key_get_hmac_key(
84 | (*[32]C.uint8_t)(unsafe.Pointer(&out)),
85 | bk.constPtr(),
86 | )
87 | if signalFfiError != nil {
88 | return out, wrapError(signalFfiError)
89 | }
90 | return out, nil
91 | }
92 |
93 | func (bk *MessageBackupKey) GetAESKey() ([32]byte, error) {
94 | var out [32]byte
95 | signalFfiError := C.signal_message_backup_key_get_aes_key(
96 | (*[32]C.uint8_t)(unsafe.Pointer(&out)),
97 | bk.constPtr(),
98 | )
99 | if signalFfiError != nil {
100 | return out, wrapError(signalFfiError)
101 | }
102 | return out, nil
103 | }
104 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/nocopy.go:
--------------------------------------------------------------------------------
1 | package libsignalgo
2 |
3 | // noCopy may be added to structs which must not be copied after the first use.
4 | // In this package, it is used for anything that uses runtime finalizers, since
5 | // the finalizer is called when the pointer is garbage collected, even if the
6 | // value is still referenced.
7 | //
8 | // See https://golang.org/issues/8005#issuecomment-190753527 for details.
9 | //
10 | // Note that it must not be embedded, due to the Lock and Unlock methods.
11 | type noCopy struct{}
12 |
13 | // Lock is a no-op used by -copylocks checker from `go vet`.
14 | func (*noCopy) Lock() {}
15 | func (*noCopy) Unlock() {}
16 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/plaintextcontent.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import "runtime"
25 |
26 | type PlaintextContent struct {
27 | nc noCopy
28 | ptr *C.SignalPlaintextContent
29 | }
30 |
31 | func wrapPlaintextContent(ptr *C.SignalPlaintextContent) *PlaintextContent {
32 | plaintextContent := &PlaintextContent{ptr: ptr}
33 | runtime.SetFinalizer(plaintextContent, (*PlaintextContent).Destroy)
34 | return plaintextContent
35 | }
36 |
37 | func PlaintextContentFromDecryptionErrorMessage(message *DecryptionErrorMessage) (*PlaintextContent, error) {
38 | var pc C.SignalMutPointerPlaintextContent
39 | signalFfiError := C.signal_plaintext_content_from_decryption_error_message(
40 | &pc,
41 | message.constPtr(),
42 | )
43 | runtime.KeepAlive(message)
44 | if signalFfiError != nil {
45 | return nil, wrapError(signalFfiError)
46 | }
47 | return wrapPlaintextContent(pc.raw), nil
48 | }
49 |
50 | func DeserializePlaintextContent(plaintextContentBytes []byte) (*PlaintextContent, error) {
51 | var pc C.SignalMutPointerPlaintextContent
52 | signalFfiError := C.signal_plaintext_content_deserialize(&pc, BytesToBuffer(plaintextContentBytes))
53 | runtime.KeepAlive(plaintextContentBytes)
54 | if signalFfiError != nil {
55 | return nil, wrapError(signalFfiError)
56 | }
57 | return wrapPlaintextContent(pc.raw), nil
58 | }
59 |
60 | func (pc *PlaintextContent) mutPtr() C.SignalMutPointerPlaintextContent {
61 | return C.SignalMutPointerPlaintextContent{pc.ptr}
62 | }
63 |
64 | func (pc *PlaintextContent) constPtr() C.SignalConstPointerPlaintextContent {
65 | return C.SignalConstPointerPlaintextContent{pc.ptr}
66 | }
67 |
68 | func (pc *PlaintextContent) Clone() (*PlaintextContent, error) {
69 | var cloned C.SignalMutPointerPlaintextContent
70 | signalFfiError := C.signal_plaintext_content_clone(
71 | &cloned,
72 | pc.constPtr(),
73 | )
74 | runtime.KeepAlive(pc)
75 | if signalFfiError != nil {
76 | return nil, wrapError(signalFfiError)
77 | }
78 | return wrapPlaintextContent(cloned.raw), nil
79 | }
80 |
81 | func (pc *PlaintextContent) Destroy() error {
82 | pc.CancelFinalizer()
83 | return wrapError(C.signal_plaintext_content_destroy(pc.mutPtr()))
84 | }
85 |
86 | func (pc *PlaintextContent) CancelFinalizer() {
87 | runtime.SetFinalizer(pc, nil)
88 | }
89 |
90 | func (pc *PlaintextContent) Serialize() ([]byte, error) {
91 | var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
92 | signalFfiError := C.signal_plaintext_content_serialize(&serialized, pc.constPtr())
93 | runtime.KeepAlive(pc)
94 | if signalFfiError != nil {
95 | return nil, wrapError(signalFfiError)
96 | }
97 | return CopySignalOwnedBufferToBytes(serialized), nil
98 | }
99 |
100 | func (pc *PlaintextContent) GetBody() ([]byte, error) {
101 | var body C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
102 | signalFfiError := C.signal_plaintext_content_get_body(&body, pc.constPtr())
103 | runtime.KeepAlive(pc)
104 | if signalFfiError != nil {
105 | return nil, wrapError(signalFfiError)
106 | }
107 | return CopySignalOwnedBufferToBytes(body), nil
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/prekeystore.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 |
23 | typedef const SignalPreKeyRecord const_pre_key_record;
24 |
25 | extern int signal_load_pre_key_callback(void *store_ctx, SignalPreKeyRecord **recordp, uint32_t id);
26 | extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, const_pre_key_record *record);
27 | extern int signal_remove_pre_key_callback(void *store_ctx, uint32_t id);
28 | */
29 | import "C"
30 | import (
31 | "context"
32 | "unsafe"
33 | )
34 |
35 | type PreKeyStore interface {
36 | LoadPreKey(ctx context.Context, id uint32) (*PreKeyRecord, error)
37 | StorePreKey(ctx context.Context, id uint32, preKeyRecord *PreKeyRecord) error
38 | RemovePreKey(ctx context.Context, id uint32) error
39 | }
40 |
41 | //export signal_load_pre_key_callback
42 | func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalPreKeyRecord, id C.uint32_t) C.int {
43 | return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
44 | key, err := store.LoadPreKey(ctx, uint32(id))
45 | if err == nil && key != nil {
46 | key.CancelFinalizer()
47 | *keyp = key.ptr
48 | }
49 | return err
50 | })
51 | }
52 |
53 | //export signal_store_pre_key_callback
54 | func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_pre_key_record) C.int {
55 | return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
56 | record := PreKeyRecord{ptr: (*C.SignalPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
57 | cloned, err := record.Clone()
58 | if err != nil {
59 | return err
60 | }
61 | return store.StorePreKey(ctx, uint32(id), cloned)
62 | })
63 | }
64 |
65 | //export signal_remove_pre_key_callback
66 | func signal_remove_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.int {
67 | return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
68 | return store.RemovePreKey(ctx, uint32(id))
69 | })
70 | }
71 |
72 | func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct {
73 | return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{
74 | ctx: wrapStore(ctx, store),
75 | load_pre_key: C.SignalLoadPreKey(C.signal_load_pre_key_callback),
76 | store_pre_key: C.SignalStorePreKey(C.signal_store_pre_key_callback),
77 | remove_pre_key: C.SignalRemovePreKey(C.signal_remove_pre_key_callback),
78 | }}
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/resources/clienthandshakestart.data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mautrix/signal/c33b32631e0fafec77a6660d9301bf1b561df88e/pkg/libsignalgo/resources/clienthandshakestart.data
--------------------------------------------------------------------------------
/pkg/libsignalgo/senderkeyrecord.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 | */
23 | import "C"
24 | import (
25 | "runtime"
26 | )
27 |
28 | type SenderKeyRecord struct {
29 | nc noCopy
30 | ptr *C.SignalSenderKeyRecord
31 | }
32 |
33 | func wrapSenderKeyRecord(ptr *C.SignalSenderKeyRecord) *SenderKeyRecord {
34 | sc := &SenderKeyRecord{ptr: ptr}
35 | runtime.SetFinalizer(sc, (*SenderKeyRecord).Destroy)
36 | return sc
37 | }
38 |
39 | func DeserializeSenderKeyRecord(serialized []byte) (*SenderKeyRecord, error) {
40 | var sc C.SignalMutPointerSenderKeyRecord
41 | signalFfiError := C.signal_sender_key_record_deserialize(&sc, BytesToBuffer(serialized))
42 | runtime.KeepAlive(serialized)
43 | if signalFfiError != nil {
44 | return nil, wrapError(signalFfiError)
45 | }
46 | return wrapSenderKeyRecord(sc.raw), nil
47 | }
48 |
49 | func (skr *SenderKeyRecord) Serialize() ([]byte, error) {
50 | var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
51 | signalFfiError := C.signal_sender_key_record_serialize(&serialized, skr.constPtr())
52 | runtime.KeepAlive(skr)
53 | if signalFfiError != nil {
54 | return nil, wrapError(signalFfiError)
55 | }
56 | return CopySignalOwnedBufferToBytes(serialized), nil
57 | }
58 |
59 | func (skr *SenderKeyRecord) mutPtr() C.SignalMutPointerSenderKeyRecord {
60 | return C.SignalMutPointerSenderKeyRecord{skr.ptr}
61 | }
62 |
63 | func (skr *SenderKeyRecord) constPtr() C.SignalConstPointerSenderKeyRecord {
64 | return C.SignalConstPointerSenderKeyRecord{skr.ptr}
65 | }
66 |
67 | func (skr *SenderKeyRecord) Clone() (*SenderKeyRecord, error) {
68 | var cloned C.SignalMutPointerSenderKeyRecord
69 | signalFfiError := C.signal_sender_key_record_clone(&cloned, skr.constPtr())
70 | runtime.KeepAlive(skr)
71 | if signalFfiError != nil {
72 | return nil, wrapError(signalFfiError)
73 | }
74 | return wrapSenderKeyRecord(cloned.raw), nil
75 | }
76 |
77 | func (skr *SenderKeyRecord) Destroy() error {
78 | skr.CancelFinalizer()
79 | return wrapError(C.signal_sender_key_record_destroy(skr.mutPtr()))
80 | }
81 |
82 | func (skr *SenderKeyRecord) CancelFinalizer() {
83 | runtime.SetFinalizer(skr, nil)
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/senderkeystore.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 |
23 | typedef const SignalProtocolAddress const_address;
24 |
25 | typedef const SignalSenderKeyRecord const_sender_key_record;
26 | typedef const uint8_t const_uuid_bytes[16];
27 |
28 | extern int signal_load_sender_key_callback(void *store_ctx, SignalSenderKeyRecord**, const_address*, const_uuid_bytes*);
29 | extern int signal_store_sender_key_callback(void *store_ctx, const_address*, const_uuid_bytes*, const_sender_key_record*);
30 | */
31 | import "C"
32 | import (
33 | "context"
34 | "unsafe"
35 |
36 | "github.com/google/uuid"
37 | )
38 |
39 | type SenderKeyStore interface {
40 | LoadSenderKey(ctx context.Context, sender *Address, distributionID uuid.UUID) (*SenderKeyRecord, error)
41 | StoreSenderKey(ctx context.Context, sender *Address, distributionID uuid.UUID, record *SenderKeyRecord) error
42 | }
43 |
44 | //export signal_load_sender_key_callback
45 | func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp **C.SignalSenderKeyRecord, address *C.const_address, distributionIDBytes *C.const_uuid_bytes) C.int {
46 | return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
47 | distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
48 | record, err := store.LoadSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID)
49 | if err == nil && record != nil {
50 | record.CancelFinalizer()
51 | *recordp = record.ptr
52 | }
53 | return err
54 | })
55 | }
56 |
57 | //export signal_store_sender_key_callback
58 | 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 {
59 | return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
60 | distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
61 | record := SenderKeyRecord{ptr: (*C.SignalSenderKeyRecord)(unsafe.Pointer(senderKeyRecord))}
62 | cloned, err := record.Clone()
63 | if err != nil {
64 | return err
65 | }
66 |
67 | return store.StoreSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID, cloned)
68 | })
69 | }
70 |
71 | func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct {
72 | return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{
73 | ctx: wrapStore(ctx, store),
74 | load_sender_key: C.SignalLoadSenderKey(C.signal_load_sender_key_callback),
75 | store_sender_key: C.SignalStoreSenderKey(C.signal_store_sender_key_callback),
76 | }}
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/serializedeserializeroundtrip_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "testing"
21 | "time"
22 |
23 | "github.com/stretchr/testify/assert"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
26 | )
27 |
28 | type Serializable interface {
29 | Serialize() ([]byte, error)
30 | }
31 |
32 | func testRoundTrip[T Serializable](t *testing.T, name string, obj T, deserializer func([]byte) (T, error)) {
33 | t.Run(name, func(t *testing.T) {
34 | serialized, err := obj.Serialize()
35 | assert.NoError(t, err)
36 |
37 | deserialized, err := deserializer(serialized)
38 | assert.NoError(t, err)
39 |
40 | deserializedSerialized, err := deserialized.Serialize()
41 | assert.NoError(t, err)
42 |
43 | assert.Equal(t, serialized, deserializedSerialized)
44 | })
45 | }
46 |
47 | // From PublicAPITests.swift:testSerializationRoundTrip
48 | func TestSenderCertificateSerializationRoundTrip(t *testing.T) {
49 | keyPair, err := libsignalgo.GenerateIdentityKeyPair()
50 | assert.NoError(t, err)
51 |
52 | testRoundTrip(t, "key pair", keyPair, libsignalgo.DeserializeIdentityKeyPair)
53 | testRoundTrip(t, "public key", keyPair.GetPublicKey(), libsignalgo.DeserializePublicKey)
54 | testRoundTrip(t, "private key", keyPair.GetPrivateKey(), libsignalgo.DeserializePrivateKey)
55 | testRoundTrip(t, "identity key", keyPair.GetIdentityKey(), libsignalgo.NewIdentityKeyFromBytes)
56 |
57 | preKeyRecord, err := libsignalgo.NewPreKeyRecord(7, keyPair.GetPublicKey(), keyPair.GetPrivateKey())
58 | assert.NoError(t, err)
59 | testRoundTrip(t, "pre key record", preKeyRecord, libsignalgo.DeserializePreKeyRecord)
60 |
61 | publicKeySerialized, err := keyPair.GetPublicKey().Serialize()
62 | assert.NoError(t, err)
63 | signature, err := keyPair.GetPrivateKey().Sign(publicKeySerialized)
64 | assert.NoError(t, err)
65 |
66 | signedPreKeyRecord, err := libsignalgo.NewSignedPreKeyRecordFromPrivateKey(
67 | 77,
68 | time.UnixMilli(42000),
69 | keyPair.GetPrivateKey(),
70 | signature,
71 | )
72 | assert.NoError(t, err)
73 | testRoundTrip(t, "signed pre key record", signedPreKeyRecord, libsignalgo.DeserializeSignedPreKeyRecord)
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/serverpublicparams.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | #include
22 | */
23 | import "C"
24 | import (
25 | "fmt"
26 | "runtime"
27 | "unsafe"
28 | )
29 |
30 | type ServerPublicParams = C.SignalServerPublicParams
31 | type NotarySignature [C.SignalSIGNATURE_LEN]byte
32 |
33 | func DeserializeServerPublicParams(params []byte) (*ServerPublicParams, error) {
34 | if len(params) != C.SignalSERVER_PUBLIC_PARAMS_LEN {
35 | return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), int(C.SignalSERVER_PUBLIC_PARAMS_LEN))
36 | }
37 | var out C.SignalMutPointerServerPublicParams
38 | signalFfiError := C.signal_server_public_params_deserialize(&out, BytesToBuffer(params[:]))
39 | if signalFfiError != nil {
40 | return nil, wrapError(signalFfiError)
41 | }
42 | return out.raw, nil
43 | }
44 |
45 | func ServerPublicParamsVerifySignature(
46 | serverPublicParams *ServerPublicParams,
47 | messageBytes []byte,
48 | NotarySignature NotarySignature,
49 | ) error {
50 | c_notarySignature := (*[C.SignalSIGNATURE_LEN]C.uint8_t)(unsafe.Pointer(&NotarySignature[0]))
51 | signalFfiError := C.signal_server_public_params_verify_signature(
52 | C.SignalConstPointerServerPublicParams{serverPublicParams},
53 | BytesToBuffer(messageBytes),
54 | c_notarySignature,
55 | )
56 | runtime.KeepAlive(messageBytes)
57 | return wrapError(signalFfiError)
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/serviceid_clang.go:
--------------------------------------------------------------------------------
1 | //go:build darwin || android || ios
2 |
3 | package libsignalgo
4 |
5 | /*
6 | #include "./libsignal-ffi.h"
7 | #include
8 | */
9 | import "C"
10 |
11 | type cPNIType = *C.SignalServiceIdFixedWidthBinaryBytes
12 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/serviceid_gcc.go:
--------------------------------------------------------------------------------
1 | //go:build !(darwin || android || ios)
2 |
3 | package libsignalgo
4 |
5 | /*
6 | #include "./libsignal-ffi.h"
7 | #include
8 | */
9 | import "C"
10 |
11 | // Hack for https://github.com/golang/go/issues/7270
12 | // The clang version is more correct, but doesn't work with gcc
13 |
14 | type cPNIType = *[17]C.uint8_t
15 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/sessionstore.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 |
23 | typedef const SignalSessionRecord const_session_record;
24 | typedef const SignalProtocolAddress const_address;
25 |
26 | extern int signal_load_session_callback(void *store_ctx, SignalSessionRecord **recordp, const_address *address);
27 | extern int signal_store_session_callback(void *store_ctx, const_address *address, const_session_record *record);
28 | */
29 | import "C"
30 | import (
31 | "context"
32 | "unsafe"
33 | )
34 |
35 | type SessionStore interface {
36 | LoadSession(ctx context.Context, address *Address) (*SessionRecord, error)
37 | StoreSession(ctx context.Context, address *Address, record *SessionRecord) error
38 | }
39 |
40 | //export signal_load_session_callback
41 | func signal_load_session_callback(storeCtx unsafe.Pointer, recordp **C.SignalSessionRecord, address *C.const_address) C.int {
42 | return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
43 | record, err := store.LoadSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))})
44 | if err == nil && record != nil {
45 | record.CancelFinalizer()
46 | *recordp = record.ptr
47 | }
48 | return err
49 | })
50 | }
51 |
52 | //export signal_store_session_callback
53 | func signal_store_session_callback(storeCtx unsafe.Pointer, address *C.const_address, sessionRecord *C.const_session_record) C.int {
54 | return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
55 | record := SessionRecord{ptr: (*C.SignalSessionRecord)(unsafe.Pointer(sessionRecord))}
56 | cloned, err := record.Clone()
57 | if err != nil {
58 | return err
59 | }
60 | return store.StoreSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, cloned)
61 | })
62 | }
63 |
64 | func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct {
65 | return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{
66 | ctx: wrapStore(ctx, store),
67 | load_session: C.SignalLoadSession(C.signal_load_session_callback),
68 | store_session: C.SignalStoreSession(C.signal_store_session_callback),
69 | }}
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/setup_test.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo_test
18 |
19 | import (
20 | "os"
21 |
22 | "github.com/rs/zerolog"
23 | "github.com/rs/zerolog/log"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
26 | )
27 |
28 | type FFILogger struct{}
29 |
30 | func (FFILogger) Enabled(target string, level libsignalgo.LogLevel) bool { return true }
31 |
32 | func (FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, message string) {
33 | var evt *zerolog.Event
34 | switch level {
35 | case libsignalgo.LogLevelError:
36 | evt = log.Error()
37 | case libsignalgo.LogLevelWarn:
38 | evt = log.Warn()
39 | case libsignalgo.LogLevelInfo:
40 | evt = log.Info()
41 | case libsignalgo.LogLevelDebug:
42 | evt = log.Debug()
43 | case libsignalgo.LogLevelTrace:
44 | evt = log.Trace()
45 | default:
46 | panic("invalid log level from libsignal")
47 | }
48 |
49 | evt.Str("component", "libsignal").
50 | Str("file", file).
51 | Uint("line", line).
52 | Msg(message)
53 | }
54 |
55 | func (FFILogger) Flush() {}
56 |
57 | var loggingSetup = false
58 |
59 | func setupLogging() {
60 | if !loggingSetup {
61 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
62 | libsignalgo.InitLogger(libsignalgo.LogLevelTrace, FFILogger{})
63 | loggingSetup = true
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/sgxclient.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "runtime"
25 | "time"
26 | )
27 |
28 | type SGXClientState struct {
29 | nc noCopy
30 | ptr *C.SignalSgxClientState
31 | }
32 |
33 | func wrapSGXClientState(ptr *C.SignalSgxClientState) *SGXClientState {
34 | cdsClientState := &SGXClientState{ptr: ptr}
35 | runtime.SetFinalizer(cdsClientState, (*SGXClientState).Destroy)
36 | return cdsClientState
37 | }
38 |
39 | func NewCDS2ClientState(mrenclave, attestationMessage []byte, currentTime time.Time) (*SGXClientState, error) {
40 | var cds C.SignalMutPointerSgxClientState
41 | signalFfiError := C.signal_cds2_client_state_new(
42 | &cds,
43 | BytesToBuffer(mrenclave),
44 | BytesToBuffer(attestationMessage),
45 | C.uint64_t(currentTime.UnixMilli()),
46 | )
47 | runtime.KeepAlive(mrenclave)
48 | runtime.KeepAlive(attestationMessage)
49 | if signalFfiError != nil {
50 | return nil, wrapError(signalFfiError)
51 | }
52 | return wrapSGXClientState(cds.raw), nil
53 | }
54 |
55 | func (cds *SGXClientState) mutPtr() C.SignalMutPointerSgxClientState {
56 | return C.SignalMutPointerSgxClientState{cds.ptr}
57 | }
58 |
59 | func (cds *SGXClientState) constPtr() C.SignalConstPointerSgxClientState {
60 | return C.SignalConstPointerSgxClientState{cds.ptr}
61 | }
62 |
63 | func (cds *SGXClientState) Destroy() error {
64 | runtime.SetFinalizer(cds, nil)
65 | return wrapError(C.signal_sgx_client_state_destroy(cds.mutPtr()))
66 | }
67 |
68 | func (cds *SGXClientState) InitialRequest() ([]byte, error) {
69 | var resp C.SignalOwnedBuffer
70 | signalFfiError := C.signal_sgx_client_state_initial_request(&resp, cds.constPtr())
71 | runtime.KeepAlive(cds)
72 | if signalFfiError != nil {
73 | return nil, wrapError(signalFfiError)
74 | }
75 | return CopySignalOwnedBufferToBytes(resp), nil
76 | }
77 |
78 | func (cds *SGXClientState) CompleteHandshake(handshakeReceived []byte) error {
79 | signalFfiError := C.signal_sgx_client_state_complete_handshake(cds.mutPtr(), BytesToBuffer(handshakeReceived))
80 | runtime.KeepAlive(cds)
81 | runtime.KeepAlive(handshakeReceived)
82 | return wrapError(signalFfiError)
83 | }
84 |
85 | func (cds *SGXClientState) EstablishedSend(plaintext []byte) ([]byte, error) {
86 | var resp C.SignalOwnedBuffer
87 | signalFfiError := C.signal_sgx_client_state_established_send(
88 | &resp,
89 | cds.mutPtr(),
90 | BytesToBuffer(plaintext),
91 | )
92 | runtime.KeepAlive(cds)
93 | runtime.KeepAlive(plaintext)
94 | if signalFfiError != nil {
95 | return nil, wrapError(signalFfiError)
96 | }
97 | return CopySignalOwnedBufferToBytes(resp), nil
98 | }
99 |
100 | func (cds *SGXClientState) EstablishedReceive(ciphertext []byte) ([]byte, error) {
101 | var resp C.SignalOwnedBuffer
102 | signalFfiError := C.signal_sgx_client_state_established_recv(
103 | &resp,
104 | cds.mutPtr(),
105 | BytesToBuffer(ciphertext),
106 | )
107 | runtime.KeepAlive(cds)
108 | runtime.KeepAlive(ciphertext)
109 | if signalFfiError != nil {
110 | return nil, wrapError(signalFfiError)
111 | }
112 | return CopySignalOwnedBufferToBytes(resp), nil
113 | }
114 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/signedprekeystore.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | // Copyright (C) 2025 Tulir Asokan
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU Affero General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU Affero General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU Affero General Public License
16 | // along with this program. If not, see .
17 |
18 | package libsignalgo
19 |
20 | /*
21 | #include "./libsignal-ffi.h"
22 |
23 | typedef const SignalSignedPreKeyRecord const_signed_pre_key_record;
24 |
25 | extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalSignedPreKeyRecord **recordp, uint32_t id);
26 | extern int signal_store_signed_pre_key_callback(void *store_ctx, uint32_t id, const_signed_pre_key_record *record);
27 | */
28 | import "C"
29 | import (
30 | "context"
31 | "unsafe"
32 | )
33 |
34 | type SignedPreKeyStore interface {
35 | LoadSignedPreKey(ctx context.Context, id uint32) (*SignedPreKeyRecord, error)
36 | StoreSignedPreKey(ctx context.Context, id uint32, signedPreKeyRecord *SignedPreKeyRecord) error
37 | }
38 |
39 | //export signal_load_signed_pre_key_callback
40 | func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalSignedPreKeyRecord, id C.uint32_t) C.int {
41 | return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
42 | key, err := store.LoadSignedPreKey(ctx, uint32(id))
43 | if err == nil && key != nil {
44 | key.CancelFinalizer()
45 | *keyp = key.ptr
46 | }
47 | return err
48 | })
49 | }
50 |
51 | //export signal_store_signed_pre_key_callback
52 | func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_signed_pre_key_record) C.int {
53 | return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
54 | record := SignedPreKeyRecord{ptr: (*C.SignalSignedPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
55 | cloned, err := record.Clone()
56 | if err != nil {
57 | return err
58 | }
59 | return store.StoreSignedPreKey(ctx, uint32(id), cloned)
60 | })
61 | }
62 |
63 | func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct {
64 | return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{
65 | ctx: wrapStore(ctx, store),
66 | load_signed_pre_key: C.SignalLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
67 | store_signed_pre_key: C.SignalStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
68 | }}
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/storeutil.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Sumner Evans
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package libsignalgo
18 |
19 | /*
20 | #include "./libsignal-ffi.h"
21 | */
22 | import "C"
23 | import (
24 | "context"
25 | "errors"
26 | "unsafe"
27 |
28 | gopointer "github.com/mattn/go-pointer"
29 | )
30 |
31 | type WrappedStore[T any] struct {
32 | Store T
33 | Ctx *CallbackContext
34 | }
35 |
36 | type CallbackContext struct {
37 | Error error
38 | Ctx context.Context
39 | Unrefs []unsafe.Pointer
40 | }
41 |
42 | func NewCallbackContext(ctx context.Context) *CallbackContext {
43 | if ctx == nil {
44 | panic(errors.New("nil context passed to NewCallbackContext"))
45 | }
46 | return &CallbackContext{Ctx: ctx}
47 | }
48 |
49 | func (ctx *CallbackContext) Unref() {
50 | for _, ptr := range ctx.Unrefs {
51 | gopointer.Unref(ptr)
52 | }
53 | }
54 |
55 | func wrapStore[T any](ctx *CallbackContext, store T) unsafe.Pointer {
56 | wrappedStore := gopointer.Save(&WrappedStore[T]{Store: store, Ctx: ctx})
57 | ctx.Unrefs = append(ctx.Unrefs, wrappedStore)
58 | return wrappedStore
59 | }
60 |
61 | func wrapStoreCallbackCustomReturn[T any](storeCtx unsafe.Pointer, callback func(store T, ctx context.Context) (int, error)) C.int {
62 | wrap := gopointer.Restore(storeCtx).(*WrappedStore[T])
63 | retVal, err := callback(wrap.Store, wrap.Ctx.Ctx)
64 | if err != nil {
65 | wrap.Ctx.Error = err
66 | }
67 | return C.int(retVal)
68 | }
69 |
70 | func wrapStoreCallback[T any](storeCtx unsafe.Pointer, callback func(store T, ctx context.Context) error) C.int {
71 | wrap := gopointer.Restore(storeCtx).(*WrappedStore[T])
72 | if err := callback(wrap.Store, wrap.Ctx.Ctx); err != nil {
73 | wrap.Ctx.Error = err
74 | return -1
75 | }
76 | return 0
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/update-ffi.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Exit on error
4 | set -e
5 |
6 | # Store the libsignal directory path
7 | LIBSIGNAL_DIRECTORY="libsignal"
8 |
9 | # Check if libsignal directory exists
10 | if [ ! -d "$LIBSIGNAL_DIRECTORY" ]; then
11 | echo "Error: Libsignal directory '$LIBSIGNAL_DIRECTORY' does not exist."
12 | exit 1
13 | fi
14 |
15 | echo "// Generated by update-ffi.sh; DO NOT EDIT." > version.go
16 | echo >> version.go
17 | echo "package libsignalgo" >> version.go
18 | echo >> version.go
19 |
20 | # Store the current working directory
21 | ORIGINAL_DIR="$(pwd)"
22 |
23 | # Navigate to libsignal directory
24 | cd "$LIBSIGNAL_DIRECTORY"
25 |
26 | echo "const Version = \"$(git describe --tags --always)\"" >> ../version.go
27 |
28 | # Build libsignal
29 | cargo build -p libsignal-ffi --release
30 |
31 | # Regenerate the header file
32 | cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
33 |
34 | # Navigate back to the original directory
35 | cd "$ORIGINAL_DIR"
36 |
37 | # Copy files from the libsignal directory
38 | cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" .
39 | cp "${LIBSIGNAL_DIRECTORY}/libsignal-ffi.h" .
40 |
41 | echo "Files copied successfully."
42 |
--------------------------------------------------------------------------------
/pkg/libsignalgo/version.go:
--------------------------------------------------------------------------------
1 | // Generated by update-ffi.sh; DO NOT EDIT.
2 |
3 | package libsignalgo
4 |
5 | const Version = "v0.72.1"
6 |
--------------------------------------------------------------------------------
/pkg/msgconv/matrixfmt/convert.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package matrixfmt
18 |
19 | import (
20 | "context"
21 |
22 | "maunium.net/go/mautrix/event"
23 |
24 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
25 | )
26 |
27 | func Parse(ctx context.Context, parser *HTMLParser, content *event.MessageEventContent) (string, []*signalpb.BodyRange) {
28 | if content.Format != event.FormatHTML {
29 | return content.Body, nil
30 | }
31 | parseCtx := NewContext(ctx)
32 | parseCtx.AllowedMentions = content.Mentions
33 | parsed := parser.Parse(content.FormattedBody, parseCtx)
34 | if parsed == nil {
35 | return "", nil
36 | }
37 | var bodyRanges []*signalpb.BodyRange
38 | if len(parsed.Entities) > 0 {
39 | bodyRanges = make([]*signalpb.BodyRange, len(parsed.Entities))
40 | for i, ent := range parsed.Entities {
41 | bodyRanges[i] = ent.Proto()
42 | }
43 | }
44 | return parsed.String.String(), bodyRanges
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/msgconv/msgconv.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package msgconv
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/google/uuid"
23 | "maunium.net/go/mautrix/bridgev2"
24 | "maunium.net/go/mautrix/bridgev2/networkid"
25 | "maunium.net/go/mautrix/id"
26 |
27 | "go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
28 | "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
29 | "go.mau.fi/mautrix-signal/pkg/signalid"
30 | "go.mau.fi/mautrix-signal/pkg/signalmeow"
31 | )
32 |
33 | type contextKey int
34 |
35 | const (
36 | contextKeyPortal contextKey = iota
37 | contextKeyClient
38 | contextKeyIntent
39 | )
40 |
41 | type MessageConverter struct {
42 | Bridge *bridgev2.Bridge
43 |
44 | SignalFmtParams *signalfmt.FormatParams
45 | MatrixFmtParams *matrixfmt.HTMLParser
46 |
47 | MaxFileSize int64
48 | LocationFormat string
49 | DisappearViewOnce bool
50 | DirectMedia bool
51 | }
52 |
53 | func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
54 | return &MessageConverter{
55 | Bridge: br,
56 | SignalFmtParams: &signalfmt.FormatParams{
57 | GetUserInfo: func(ctx context.Context, uuid uuid.UUID) signalfmt.UserInfo {
58 | ghost, err := br.GetGhostByID(ctx, signalid.MakeUserID(uuid))
59 | if err != nil {
60 | // TODO log?
61 | return signalfmt.UserInfo{}
62 | }
63 | userInfo := signalfmt.UserInfo{
64 | MXID: ghost.Intent.GetMXID(),
65 | Name: ghost.Name,
66 | }
67 | userLogin := br.GetCachedUserLoginByID(networkid.UserLoginID(uuid.String()))
68 | if userLogin != nil {
69 | userInfo.MXID = userLogin.UserMXID
70 | // TODO find matrix user displayname?
71 | }
72 | return userInfo
73 | },
74 | },
75 | MatrixFmtParams: &matrixfmt.HTMLParser{
76 | GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
77 | parsed, ok := br.Matrix.ParseGhostMXID(userID)
78 | if ok {
79 | u, _ := uuid.Parse(string(parsed))
80 | return u
81 | }
82 | user, _ := br.GetExistingUserByMXID(ctx, userID)
83 | // TODO log errors?
84 | if user != nil {
85 | preferredLogin, _, _ := getPortal(ctx).FindPreferredLogin(ctx, user, true)
86 | if preferredLogin != nil {
87 | u, _ := uuid.Parse(string(preferredLogin.ID))
88 | return u
89 | }
90 | }
91 | return uuid.Nil
92 | },
93 | },
94 | MaxFileSize: 50 * 1024 * 1024,
95 | }
96 | }
97 |
98 | func getClient(ctx context.Context) *signalmeow.Client {
99 | return ctx.Value(contextKeyClient).(*signalmeow.Client)
100 | }
101 |
102 | func getPortal(ctx context.Context) *bridgev2.Portal {
103 | return ctx.Value(contextKeyPortal).(*bridgev2.Portal)
104 | }
105 |
106 | func getIntent(ctx context.Context) bridgev2.MatrixAPI {
107 | return ctx.Value(contextKeyIntent).(bridgev2.MatrixAPI)
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/msgconv/signalfmt/convert.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalfmt
18 |
19 | import (
20 | "context"
21 | "html"
22 | "slices"
23 | "strings"
24 |
25 | "github.com/google/uuid"
26 | "golang.org/x/exp/maps"
27 | "maunium.net/go/mautrix/event"
28 | "maunium.net/go/mautrix/id"
29 |
30 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
31 | )
32 |
33 | type UserInfo struct {
34 | MXID id.UserID
35 | Name string
36 | }
37 |
38 | type FormatParams struct {
39 | GetUserInfo func(ctx context.Context, uuid uuid.UUID) UserInfo
40 | }
41 |
42 | type formatContext struct {
43 | IsInCodeblock bool
44 | }
45 |
46 | func (ctx formatContext) TextToHTML(text string) string {
47 | if ctx.IsInCodeblock {
48 | return html.EscapeString(text)
49 | }
50 | return event.TextToHTML(text)
51 | }
52 |
53 | func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, params *FormatParams) *event.MessageEventContent {
54 | content := &event.MessageEventContent{
55 | MsgType: event.MsgText,
56 | Body: message,
57 | Mentions: &event.Mentions{},
58 | }
59 | if len(ranges) == 0 {
60 | return content
61 | }
62 | // LinkedRangeTree.Add depends on the ranges being sorted by increasing start index and then decreasing length.
63 | slices.SortFunc(ranges, func(a, b *signalpb.BodyRange) int {
64 | if *a.Start == *b.Start {
65 | if *a.Length == *b.Length {
66 | return 0
67 | } else if *a.Length < *b.Length {
68 | return 1
69 | } else {
70 | return -1
71 | }
72 | } else if *a.Start < *b.Start {
73 | return -1
74 | } else {
75 | return 1
76 | }
77 | })
78 |
79 | lrt := &LinkedRangeTree{}
80 | mentions := map[id.UserID]struct{}{}
81 | utf16Message := NewUTF16String(message)
82 | maxLength := len(utf16Message)
83 | for _, r := range ranges {
84 | br := BodyRange{
85 | Start: int(*r.Start),
86 | Length: int(*r.Length),
87 | }.TruncateEnd(maxLength)
88 | switch rv := r.GetAssociatedValue().(type) {
89 | case *signalpb.BodyRange_Style_:
90 | br.Value = Style(rv.Style)
91 | case *signalpb.BodyRange_MentionAci:
92 | parsed, err := uuid.Parse(rv.MentionAci)
93 | if err != nil {
94 | continue
95 | }
96 | userInfo := params.GetUserInfo(ctx, parsed)
97 | if userInfo.MXID == "" {
98 | continue
99 | }
100 | mentions[userInfo.MXID] = struct{}{}
101 | // This could replace the wrong thing if there's a mention without fffc.
102 | // Maybe use NewUTF16String and do index replacements for the plaintext body too,
103 | // or just replace the plaintext body by parsing the generated HTML.
104 | content.Body = strings.Replace(content.Body, "\uFFFC", userInfo.Name, 1)
105 | br.Value = Mention{UserInfo: userInfo, UUID: parsed}
106 | }
107 | lrt.Add(br)
108 | }
109 |
110 | content.Mentions.UserIDs = maps.Keys(mentions)
111 | content.FormattedBody = lrt.Format(utf16Message, formatContext{})
112 | content.Format = event.FormatHTML
113 | //content.Body = format.HTMLToText(content.FormattedBody)
114 | return content
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/msgconv/signalfmt/html.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalfmt
18 |
19 | import (
20 | "fmt"
21 | "strings"
22 | "unicode/utf16"
23 | )
24 |
25 | func (m Mention) Format(message string) string {
26 | return fmt.Sprintf(
27 | `%s`,
28 | m.MXID.URI().MatrixToURL(),
29 | strings.Replace(message, "\ufffc", m.Name, 1),
30 | )
31 | }
32 |
33 | func (s Style) Format(message string) string {
34 | switch s {
35 | case StyleBold:
36 | return fmt.Sprintf("%s", message)
37 | case StyleItalic:
38 | return fmt.Sprintf("%s", message)
39 | case StyleSpoiler:
40 | return fmt.Sprintf("%s", message)
41 | case StyleStrikethrough:
42 | return fmt.Sprintf("%s", message)
43 | case StyleMonospace:
44 | if strings.ContainsRune(message, '\n') {
45 | // This is somewhat incorrect, as it won't allow inline text before/after a multiline monospace-formatted string.
46 | return fmt.Sprintf("%s
", message)
47 | }
48 | return fmt.Sprintf("%s
", message)
49 | default:
50 | return message
51 | }
52 | }
53 |
54 | type UTF16String []uint16
55 |
56 | func NewUTF16String(s string) UTF16String {
57 | return utf16.Encode([]rune(s))
58 | }
59 |
60 | func (u UTF16String) String() string {
61 | return string(utf16.Decode(u))
62 | }
63 |
64 | func (lrt *LinkedRangeTree) Format(message UTF16String, ctx formatContext) string {
65 | if lrt == nil || lrt.Node == nil {
66 | return ctx.TextToHTML(message.String())
67 | }
68 | head := message[:lrt.Node.Start]
69 | headStr := ctx.TextToHTML(head.String())
70 | inner := message[lrt.Node.Start:lrt.Node.End()]
71 | tail := message[lrt.Node.End():]
72 | ourCtx := ctx
73 | if lrt.Node.Value == StyleMonospace {
74 | ourCtx.IsInCodeblock = true
75 | }
76 | childMessage := lrt.Child.Format(inner, ourCtx)
77 | formattedChildMessage := lrt.Node.Value.Format(childMessage)
78 | siblingMessage := lrt.Sibling.Format(tail, ctx)
79 | return headStr + formattedChildMessage + siblingMessage
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/msgconv/signalfmt/tags.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalfmt
18 |
19 | import (
20 | "fmt"
21 |
22 | "github.com/google/uuid"
23 |
24 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
25 | )
26 |
27 | type BodyRangeValue interface {
28 | String() string
29 | Format(message string) string
30 | Proto() signalpb.BodyRangeAssociatedValue
31 | }
32 |
33 | type Mention struct {
34 | UserInfo
35 | UUID uuid.UUID
36 | }
37 |
38 | func (m Mention) String() string {
39 | return fmt.Sprintf("Mention{MXID: id.UserID(%q), Name: %q}", m.MXID, m.Name)
40 | }
41 |
42 | func (m Mention) Proto() signalpb.BodyRangeAssociatedValue {
43 | return &signalpb.BodyRange_MentionAci{
44 | MentionAci: m.UUID.String(),
45 | }
46 | }
47 |
48 | type Style int
49 |
50 | const (
51 | StyleNone Style = iota
52 | StyleBold
53 | StyleItalic
54 | StyleSpoiler
55 | StyleStrikethrough
56 | StyleMonospace
57 | )
58 |
59 | func (s Style) Proto() signalpb.BodyRangeAssociatedValue {
60 | return &signalpb.BodyRange_Style_{
61 | Style: signalpb.BodyRange_Style(s),
62 | }
63 | }
64 |
65 | func (s Style) String() string {
66 | switch s {
67 | case StyleNone:
68 | return "StyleNone"
69 | case StyleBold:
70 | return "StyleBold"
71 | case StyleItalic:
72 | return "StyleItalic"
73 | case StyleSpoiler:
74 | return "StyleSpoiler"
75 | case StyleStrikethrough:
76 | return "StyleStrikethrough"
77 | case StyleMonospace:
78 | return "StyleMonospace"
79 | default:
80 | return fmt.Sprintf("Style(%d)", s)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/msgconv/signalfmt/tree.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalfmt
18 |
19 | import (
20 | "fmt"
21 | "sort"
22 |
23 | "google.golang.org/protobuf/proto"
24 |
25 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
26 | )
27 |
28 | type BodyRange struct {
29 | Start int
30 | Length int
31 | Value BodyRangeValue
32 | }
33 |
34 | type BodyRangeList []BodyRange
35 |
36 | var _ sort.Interface = BodyRangeList(nil)
37 |
38 | func (b BodyRangeList) Len() int {
39 | return len(b)
40 | }
41 |
42 | func (b BodyRangeList) Less(i, j int) bool {
43 | return b[i].Start < b[j].Start || b[i].Length > b[j].Length
44 | }
45 |
46 | func (b BodyRangeList) Swap(i, j int) {
47 | b[i], b[j] = b[j], b[i]
48 | }
49 |
50 | func (b BodyRange) String() string {
51 | return fmt.Sprintf("%d:%d:%v", b.Start, b.Length, b.Value)
52 | }
53 |
54 | // End returns the end index of the range.
55 | func (b BodyRange) End() int {
56 | return b.Start + b.Length
57 | }
58 |
59 | // Offset changes the start of the range without affecting the length.
60 | func (b BodyRange) Offset(offset int) *BodyRange {
61 | b.Start += offset
62 | return &b
63 | }
64 |
65 | // TruncateStart changes the length of the range, so it starts at the given index and ends at the same index as before.
66 | func (b BodyRange) TruncateStart(startAt int) *BodyRange {
67 | if b.Start < startAt {
68 | b.Length -= startAt - b.Start
69 | b.Start = startAt
70 | }
71 | return &b
72 | }
73 |
74 | // TruncateEnd changes the length of the range, so it ends at or before the given index and starts at the same index as before.
75 | func (b BodyRange) TruncateEnd(maxEnd int) *BodyRange {
76 | if b.End() > maxEnd {
77 | b.Length = maxEnd - b.Start
78 | }
79 | return &b
80 | }
81 |
82 | func (b BodyRange) Proto() *signalpb.BodyRange {
83 | return &signalpb.BodyRange{
84 | Start: proto.Uint32(uint32(b.Start)),
85 | Length: proto.Uint32(uint32(b.Length)),
86 | AssociatedValue: b.Value.Proto(),
87 | }
88 | }
89 |
90 | // LinkedRangeTree is a linked tree of formatting entities.
91 | //
92 | // It's meant to parse a list of Signal body ranges into nodes that either overlap completely or not at all,
93 | // which enables more natural conversion to HTML.
94 | type LinkedRangeTree struct {
95 | Node *BodyRange
96 | Sibling *LinkedRangeTree
97 | Child *LinkedRangeTree
98 | }
99 |
100 | func ptrAdd(to **LinkedRangeTree, r *BodyRange) {
101 | if *to == nil {
102 | *to = &LinkedRangeTree{}
103 | }
104 | (*to).Add(r)
105 | }
106 |
107 | // Add adds the given formatting entity to this tree.
108 | func (lrt *LinkedRangeTree) Add(r *BodyRange) {
109 | if lrt.Node == nil {
110 | lrt.Node = r
111 | return
112 | }
113 | lrtEnd := lrt.Node.End()
114 | if r.Start >= lrtEnd {
115 | ptrAdd(&lrt.Sibling, r.Offset(-lrtEnd))
116 | return
117 | }
118 | if r.End() > lrtEnd {
119 | ptrAdd(&lrt.Sibling, r.TruncateStart(lrtEnd).Offset(-lrtEnd))
120 | }
121 | ptrAdd(&lrt.Child, r.TruncateEnd(lrtEnd).Offset(-lrt.Node.Start))
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/msgconv/urlpreview.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package msgconv
18 |
19 | import (
20 | "context"
21 | "time"
22 |
23 | "github.com/rs/zerolog"
24 | "google.golang.org/protobuf/proto"
25 | "maunium.net/go/mautrix/event"
26 |
27 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
28 | )
29 |
30 | func (mc *MessageConverter) convertURLPreviewsToBeeper(ctx context.Context, preview []*signalpb.Preview, attMap AttachmentMap) []*event.BeeperLinkPreview {
31 | output := make([]*event.BeeperLinkPreview, len(preview))
32 | for i, p := range preview {
33 | output[i] = mc.convertURLPreviewToBeeper(ctx, p, attMap)
34 | }
35 | return output
36 | }
37 |
38 | func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, preview *signalpb.Preview, attMap AttachmentMap) *event.BeeperLinkPreview {
39 | output := &event.BeeperLinkPreview{
40 | MatchedURL: preview.GetUrl(),
41 | LinkPreview: event.LinkPreview{
42 | CanonicalURL: preview.GetUrl(),
43 | Title: preview.GetTitle(),
44 | Description: preview.GetDescription(),
45 | },
46 | }
47 | if preview.Image != nil {
48 | msg, err := mc.reuploadAttachment(ctx, preview.Image, attMap)
49 | if err != nil {
50 | zerolog.Ctx(ctx).Err(err).Msg("Failed to reupload link preview image")
51 | } else {
52 | output.ImageURL = msg.Content.URL
53 | output.ImageEncryption = msg.Content.File
54 | output.ImageType = msg.Content.Info.MimeType
55 | output.ImageSize = event.IntOrString(msg.Content.Info.Size)
56 | output.ImageHeight = event.IntOrString(msg.Content.Info.Height)
57 | output.ImageWidth = event.IntOrString(msg.Content.Info.Width)
58 | }
59 | }
60 | return output
61 | }
62 |
63 | func (mc *MessageConverter) convertURLPreviewToSignal(ctx context.Context, content *event.MessageEventContent) []*signalpb.Preview {
64 | if len(content.BeeperLinkPreviews) == 0 {
65 | return nil
66 | }
67 | output := make([]*signalpb.Preview, len(content.BeeperLinkPreviews))
68 | for i, preview := range content.BeeperLinkPreviews {
69 | output[i] = &signalpb.Preview{
70 | Url: proto.String(preview.MatchedURL),
71 | Title: proto.String(preview.Title),
72 | Description: proto.String(preview.Description),
73 | Date: proto.Uint64(uint64(time.Now().UnixMilli())),
74 | }
75 | if preview.ImageURL != "" || preview.ImageEncryption != nil {
76 | data, err := mc.Bridge.Bot.DownloadMedia(ctx, preview.ImageURL, preview.ImageEncryption)
77 | if err != nil {
78 | zerolog.Ctx(ctx).Err(err).Int("preview_index", i).Msg("Failed to download URL preview image")
79 | continue
80 | }
81 | uploaded, err := getClient(ctx).UploadAttachment(ctx, data)
82 | if err != nil {
83 | zerolog.Ctx(ctx).Err(err).Int("preview_index", i).Msg("Failed to reupload URL preview image")
84 | continue
85 | }
86 | uploaded.ContentType = proto.String(preview.ImageType)
87 | uploaded.Width = proto.Uint32(uint32(preview.ImageWidth))
88 | uploaded.Height = proto.Uint32(uint32(preview.ImageHeight))
89 | output[i].Image = uploaded
90 | }
91 | }
92 | return output
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/signalid/dbmeta.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalid
18 |
19 | import (
20 | "go.mau.fi/util/jsontime"
21 | )
22 |
23 | type PortalMetadata struct {
24 | Revision uint32 `json:"revision,omitempty"`
25 | ExpirationTimerVersion uint32 `json:"expiration_timer_version,omitempty"`
26 | }
27 |
28 | type MessageMetadata struct {
29 | ContainsAttachments bool `json:"contains_attachments,omitempty"`
30 | }
31 |
32 | type UserLoginMetadata struct {
33 | ChatsSynced bool `json:"chats_synced,omitempty"`
34 | }
35 |
36 | type GhostMetadata struct {
37 | ProfileFetchedAt jsontime.UnixMilli `json:"profile_fetched_at"`
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/signalmeow/client.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalmeow
18 |
19 | import (
20 | "context"
21 | "errors"
22 | "net/url"
23 | "sync"
24 | "time"
25 |
26 | "github.com/rs/zerolog"
27 |
28 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
29 | "go.mau.fi/mautrix-signal/pkg/signalmeow/events"
30 | "go.mau.fi/mautrix-signal/pkg/signalmeow/store"
31 | "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
32 | )
33 |
34 | type Client struct {
35 | Store *store.Device
36 | Log zerolog.Logger
37 |
38 | SenderCertificateWithE164 *libsignalgo.SenderCertificate
39 | SenderCertificateNoE164 *libsignalgo.SenderCertificate
40 | GroupCredentials *GroupCredentials
41 | GroupCache *GroupCache
42 | ProfileCache *ProfileCache
43 | GroupCallCache *map[string]bool
44 | LastContactRequestTime time.Time
45 | SyncContactsOnConnect bool
46 |
47 | encryptionLock sync.Mutex
48 |
49 | AuthedWS *web.SignalWebsocket
50 | UnauthedWS *web.SignalWebsocket
51 | lastConnectionStatus SignalConnectionStatus
52 |
53 | loopCancel context.CancelFunc
54 | loopWg sync.WaitGroup
55 |
56 | EventHandler func(events.SignalEvent)
57 |
58 | storageAuthLock sync.Mutex
59 | storageAuth *basicExpiringCredentials
60 | cdAuthLock sync.Mutex
61 | cdAuth *basicExpiringCredentials
62 | cdToken []byte
63 |
64 | writeCallbackCounter chan time.Time
65 | }
66 |
67 | func (cli *Client) handleEvent(evt events.SignalEvent) {
68 | if cli.EventHandler != nil {
69 | cli.EventHandler(evt)
70 | }
71 | }
72 |
73 | func (cli *Client) IsConnected() bool {
74 | if cli == nil {
75 | return false
76 | }
77 | return cli.AuthedWS.IsConnected() && cli.UnauthedWS.IsConnected()
78 | }
79 |
80 | func (cli *Client) connectAuthedWS(ctx context.Context, requestHandler web.RequestHandlerFunc) (chan web.SignalWebsocketConnectionStatus, error) {
81 | if cli.AuthedWS != nil {
82 | return nil, errors.New("authed websocket already connected")
83 | }
84 |
85 | username, password := cli.Store.BasicAuthCreds()
86 | log := zerolog.Ctx(ctx).With().
87 | Str("websocket_type", "authed").
88 | Str("username", username).
89 | Logger()
90 | ctx = log.WithContext(ctx)
91 | authedWS := web.NewSignalWebsocket(url.UserPassword(username, password))
92 | statusChan := authedWS.Connect(ctx, requestHandler)
93 | cli.AuthedWS = authedWS
94 | return statusChan, nil
95 | }
96 |
97 | func (cli *Client) connectUnauthedWS(ctx context.Context) (chan web.SignalWebsocketConnectionStatus, error) {
98 | if cli.UnauthedWS != nil {
99 | return nil, errors.New("unauthed websocket already connected")
100 | }
101 |
102 | log := zerolog.Ctx(ctx).With().
103 | Str("websocket_type", "unauthed").
104 | Logger()
105 | ctx = log.WithContext(ctx)
106 | unauthedWS := web.NewSignalWebsocket(nil)
107 | statusChan := unauthedWS.Connect(ctx, nil)
108 | cli.UnauthedWS = unauthedWS
109 | return statusChan, nil
110 | }
111 |
112 | func (cli *Client) IsLoggedIn() bool {
113 | return cli.Store != nil && cli.Store.IsDeviceLoggedIn()
114 | }
115 |
--------------------------------------------------------------------------------
/pkg/signalmeow/events/message.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package events
18 |
19 | import (
20 | "github.com/google/uuid"
21 |
22 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
23 | signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
24 | "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
25 | )
26 |
27 | type SignalEvent interface {
28 | isSignalEvent()
29 | }
30 |
31 | func (*ChatEvent) isSignalEvent() {}
32 | func (*DecryptionError) isSignalEvent() {}
33 | func (*Receipt) isSignalEvent() {}
34 | func (*ReadSelf) isSignalEvent() {}
35 | func (*Call) isSignalEvent() {}
36 | func (*ContactList) isSignalEvent() {}
37 | func (*ACIFound) isSignalEvent() {}
38 | func (*QueueEmpty) isSignalEvent() {}
39 |
40 | type MessageInfo struct {
41 | Sender uuid.UUID
42 | ChatID string
43 |
44 | GroupRevision uint32
45 | ServerTimestamp uint64
46 | }
47 |
48 | type ChatEvent struct {
49 | Info MessageInfo
50 | Event signalpb.ChatEventContent
51 | }
52 |
53 | type DecryptionError struct {
54 | Sender uuid.UUID
55 | Err error
56 | Timestamp uint64
57 | }
58 |
59 | type Receipt struct {
60 | Sender uuid.UUID
61 | Content *signalpb.ReceiptMessage
62 | }
63 |
64 | type ReadSelf struct {
65 | Messages []*signalpb.SyncMessage_Read
66 | }
67 |
68 | type Call struct {
69 | Info MessageInfo
70 | Timestamp uint64
71 | IsRinging bool
72 | }
73 |
74 | type ContactList struct {
75 | Contacts []*types.Recipient
76 | }
77 |
78 | type ACIFound struct {
79 | PNI libsignalgo.ServiceID
80 | ACI libsignalgo.ServiceID
81 | }
82 |
83 | type QueueEmpty struct{}
84 |
--------------------------------------------------------------------------------
/pkg/signalmeow/misc.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalmeow
18 |
19 | import (
20 | _ "embed"
21 |
22 | "github.com/rs/zerolog"
23 | "go.mau.fi/util/exerrors"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
26 | )
27 |
28 | var loggingSetup = false
29 |
30 | func SetLogger(l zerolog.Logger) {
31 | if loggingSetup {
32 | return
33 | }
34 | libsignalgo.InitLogger(libsignalgo.LogLevelInfo, FFILogger{
35 | logger: l,
36 | })
37 | loggingSetup = true
38 | }
39 |
40 | type FFILogger struct {
41 | logger zerolog.Logger
42 | }
43 |
44 | func (l FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, message string) {
45 | var evt *zerolog.Event
46 | switch level {
47 | case libsignalgo.LogLevelError:
48 | evt = l.logger.Error()
49 | case libsignalgo.LogLevelWarn:
50 | evt = l.logger.Warn()
51 | case libsignalgo.LogLevelInfo:
52 | evt = l.logger.Info()
53 | case libsignalgo.LogLevelDebug:
54 | evt = l.logger.Debug()
55 | case libsignalgo.LogLevelTrace:
56 | evt = l.logger.Trace()
57 | default:
58 | panic("invalid log level from libsignal")
59 | }
60 |
61 | evt.Str("component", "libsignal").
62 | Str("file", file).
63 | Uint("line", line).
64 | Msg(message)
65 | }
66 |
67 | func (FFILogger) Flush() {}
68 |
69 | // Ensure FFILogger implements the Logger interface
70 | var _ libsignalgo.Logger = FFILogger{}
71 |
72 | //go:embed prod-server-public-params.dat
73 | var prodServerPublicParamsSlice []byte
74 | var prodServerPublicParams *libsignalgo.ServerPublicParams
75 |
76 | func init() {
77 | prodServerPublicParams = exerrors.Must(libsignalgo.DeserializeServerPublicParams(prodServerPublicParamsSlice))
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/signalmeow/prod-server-public-params.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mautrix/signal/c33b32631e0fafec77a6660d9301bf1b561df88e/pkg/signalmeow/prod-server-public-params.dat
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/ContactDiscovery.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Signal Messenger, LLC
2 | // SPDX-License-Identifier: AGPL-3.0-only
3 |
4 | package signalservice;
5 |
6 | message CDSClientRequest {
7 | // Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
8 | // by its 16-byte UAK.
9 | optional bytes aci_uak_pairs = 1;
10 |
11 | // Each E164 is an 8-byte big-endian number, as 8 bytes.
12 | optional bytes prev_e164s = 2;
13 | optional bytes new_e164s = 3;
14 | optional bytes discard_e164s = 4;
15 |
16 | // If true, the client has more pairs or e164s to send. If false or unset,
17 | // this is the client's last request, and processing should commence.
18 | optional bool has_more = 5;
19 |
20 | // If set, a token which allows rate limiting to discount the e164s in
21 | // the request's prev_e164s, only counting new_e164s. If not set, then
22 | // rate limiting considers both prev_e164s' and new_e164s' size.
23 | optional bytes token = 6;
24 |
25 | // After receiving a new token from the server, send back a message just
26 | // containing a token_ack.
27 | optional bool token_ack = 7;
28 |
29 | // Request that, if the server allows, both ACI and PNI be returned even
30 | // if the aci_uak_pairs don't match.
31 | optional bool return_acis_without_uaks = 8;
32 | }
33 |
34 | message CDSClientResponse {
35 | // Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
36 | // If the e164 was not found, PNI and ACI are all zeros. If the PNI
37 | // was found but the ACI was not, the PNI will be non-zero and the ACI
38 | // will be all zeros. ACI will be returned if one of the returned
39 | // PNIs has an ACI/UAK pair that matches.
40 | //
41 | // Should the request be successful (IE: a successful status returned),
42 | // |e164_pni_aci_triple| will always equal |e164| of the request,
43 | // so the entire marshalled size of the response will be (2+32)*|e164|,
44 | // where the additional 2 bytes are the id/type/length additions of the
45 | // protobuf marshaling added to each byte array. This avoids any data
46 | // leakage based on the size of the encrypted output.
47 | optional bytes e164_pni_aci_triples = 1;
48 |
49 | // A token which allows subsequent calls' rate limiting to discount the
50 | // e164s sent up in this request, only counting those in the next
51 | // request's new_e164s.
52 | optional bytes token = 3;
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/DeviceName.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Signal Messenger, LLC
2 | // SPDX-License-Identifier: AGPL-3.0-only
3 |
4 | package signalservice;
5 |
6 | message DeviceName {
7 | optional bytes ephemeralPublic = 1;
8 | optional bytes syntheticIv = 2;
9 | optional bytes ciphertext = 3;
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/Provisioning.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2014-2016 Open Whisper Systems
3 | *
4 | * Licensed according to the LICENSE file in this repository.
5 | */
6 | syntax = "proto2";
7 |
8 | package signalservice;
9 |
10 | option java_package = "org.whispersystems.signalservice.internal.push";
11 | option java_outer_classname = "ProvisioningProtos";
12 |
13 | message ProvisioningAddress {
14 | optional string address = 1;
15 | }
16 |
17 | message ProvisionEnvelope {
18 | optional bytes publicKey = 1;
19 | optional bytes body = 2; // Encrypted ProvisionMessage
20 | }
21 |
22 | message ProvisionMessage {
23 | optional bytes aciIdentityKeyPublic = 1;
24 | optional bytes aciIdentityKeyPrivate = 2;
25 | optional bytes pniIdentityKeyPublic = 11;
26 | optional bytes pniIdentityKeyPrivate = 12;
27 | optional string aci = 8;
28 | optional string pni = 10;
29 | optional string number = 3;
30 | optional string provisioningCode = 4;
31 | optional string userAgent = 5;
32 | optional bytes profileKey = 6;
33 | optional bool readReceipts = 7;
34 | optional uint32 provisioningVersion = 9;
35 | optional bytes masterKey = 13;
36 | optional bytes ephemeralBackupKey = 14; // 32 bytes
37 | optional string accountEntropyPool = 15;
38 | optional bytes mediaRootBackupKey = 16; // 32-bytes
39 | // NEXT ID: 17
40 | }
41 |
42 | enum ProvisioningVersion {
43 | option allow_alias = true;
44 |
45 | INITIAL = 0;
46 | TABLET_SUPPORT = 1;
47 | CURRENT = 1;
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/StickerResources.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2019 Open Whisper Systems
3 | *
4 | * Licensed according to the LICENSE file in this repository.
5 | */
6 | syntax = "proto2";
7 |
8 | package signalservice;
9 |
10 | option java_package = "org.whispersystems.signalservice.internal.sticker";
11 | option java_outer_classname = "StickerProtos";
12 |
13 | message Pack {
14 | message Sticker {
15 | optional uint32 id = 1;
16 | optional string emoji = 2;
17 | optional string contentType = 3;
18 | }
19 |
20 | optional string title = 1;
21 | optional string author = 2;
22 | optional Sticker cover = 3;
23 | repeated Sticker stickers = 4;
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/UnidentifiedDelivery.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Signal Messenger, LLC
2 | // SPDX-License-Identifier: AGPL-3.0-only
3 |
4 | package signalservice;
5 |
6 | option java_package = "org.whispersystems.libsignal.protocol";
7 | option java_outer_classname = "WhisperProtos";
8 |
9 | message ServerCertificate {
10 | message Certificate {
11 | optional uint32 id = 1;
12 | optional bytes key = 2;
13 | }
14 |
15 | optional bytes certificate = 1;
16 | optional bytes signature = 2;
17 | }
18 |
19 | message SenderCertificate {
20 | message Certificate {
21 | optional string senderE164 = 1;
22 | optional string senderUuid = 6;
23 | optional uint32 senderDevice = 2;
24 | optional fixed64 expires = 3;
25 | optional bytes identityKey = 4;
26 | optional ServerCertificate signer = 5;
27 | }
28 |
29 | optional bytes certificate = 1;
30 | optional bytes signature = 2;
31 | }
32 |
33 | message UnidentifiedSenderMessage {
34 |
35 | message Message {
36 | enum Type {
37 | PREKEY_MESSAGE = 1;
38 | MESSAGE = 2;
39 | // Further cases should line up with Envelope.Type, even though old cases don't.
40 |
41 | // Our parser does not handle reserved in enums: DESKTOP-1569
42 | // reserved 3 to 6;
43 |
44 | SENDERKEY_MESSAGE = 7;
45 | PLAINTEXT_CONTENT = 8;
46 | }
47 |
48 | enum ContentHint {
49 | // Show an error immediately; it was important but we can't retry.
50 | DEFAULT = 0;
51 |
52 | // Sender will try to resend; delay any error UI if possible
53 | RESENDABLE = 1;
54 |
55 | // Don't show any error UI at all; this is something sent implicitly like a typing message or a receipt
56 | IMPLICIT = 2;
57 | }
58 |
59 | optional Type type = 1;
60 | optional SenderCertificate senderCertificate = 2;
61 | optional bytes content = 3;
62 | optional ContentHint contentHint = 4;
63 | optional bytes groupId = 5;
64 | }
65 |
66 | optional bytes ephemeralPublic = 1;
67 | optional bytes encryptedStatic = 2;
68 | optional bytes encryptedMessage = 3;
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/WebSocketResources.proto:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2014-2016 Open Whisper Systems
3 | *
4 | * Licensed according to the LICENSE file in this repository.
5 | */
6 | syntax = "proto2";
7 |
8 | package signalservice;
9 |
10 | option java_package = "org.whispersystems.signalservice.internal.websocket";
11 | option java_outer_classname = "WebSocketProtos";
12 |
13 | message WebSocketRequestMessage {
14 | optional string verb = 1;
15 | optional string path = 2;
16 | optional bytes body = 3;
17 | repeated string headers = 5;
18 | optional uint64 id = 4;
19 | }
20 |
21 | message WebSocketResponseMessage {
22 | optional uint64 id = 1;
23 | optional uint32 status = 2;
24 | optional string message = 3;
25 | repeated string headers = 5;
26 | optional bytes body = 4;
27 | }
28 |
29 | message WebSocketMessage {
30 | enum Type {
31 | UNKNOWN = 0;
32 | REQUEST = 1;
33 | RESPONSE = 2;
34 | }
35 |
36 | optional Type type = 1;
37 | optional WebSocketRequestMessage request = 2;
38 | optional WebSocketResponseMessage response = 3;
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/backuppb/Backup.pb.raw:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mautrix/signal/c33b32631e0fafec77a6660d9301bf1b561df88e/pkg/signalmeow/protobuf/backuppb/Backup.pb.raw
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/build-protos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PKG_IMPORT_PATH="go.mau.fi/mautrix-signal/pkg/signalmeow/signalpb"
3 | for file in *.proto
4 | do
5 | # Requires https://go-review.googlesource.com/c/protobuf/+/369634
6 | protoc --go_out=. \
7 | --go_opt=M${file}=$PKG_IMPORT_PATH \
8 | --go_opt=paths=source_relative \
9 | $file
10 | done
11 | protoc --go_out=. \
12 | --go_opt=Mbackuppb/Backup.proto=$PKG_IMPORT_PATH/backuppb \
13 | --go_opt=paths=source_relative \
14 | backuppb/Backup.proto
15 | pre-commit run -a
16 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/extra.go:
--------------------------------------------------------------------------------
1 | package signalpb
2 |
3 | type BodyRangeAssociatedValue = isBodyRange_AssociatedValue
4 |
5 | type ChatEventContent interface {
6 | isChatEventContent()
7 | }
8 |
9 | func (*DataMessage) isChatEventContent() {}
10 | func (*TypingMessage) isChatEventContent() {}
11 | func (*EditMessage) isChatEventContent() {}
12 |
--------------------------------------------------------------------------------
/pkg/signalmeow/protobuf/update-protos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | ANDROID_GIT_REVISION=${1:-68058264729cb237bd8523df404d9455cb3f622f}
5 | DESKTOP_GIT_REVISION=${1:-c861161f22e553ecb81982bc2c7b7d495ee42fcc}
6 |
7 | update_proto() {
8 | case "$1" in
9 | Signal-Android)
10 | REPO="Signal-Android"
11 | prefix="libsignal-service/src/main/protowire/"
12 | GIT_REVISION=$ANDROID_GIT_REVISION
13 | ;;
14 | Signal-Android-App)
15 | REPO="Signal-Android"
16 | prefix="app/src/main/protowire/"
17 | GIT_REVISION=$ANDROID_GIT_REVISION
18 | ;;
19 | Signal-Desktop)
20 | REPO="Signal-Desktop"
21 | prefix="protos/"
22 | GIT_REVISION=$DESKTOP_GIT_REVISION
23 | ;;
24 | esac
25 | echo https://raw.githubusercontent.com/signalapp/${REPO}/${GIT_REVISION}/${prefix}${2}
26 | curl -LOf https://raw.githubusercontent.com/signalapp/${REPO}/${GIT_REVISION}/${prefix}${2}
27 | }
28 |
29 |
30 | update_proto Signal-Android Groups.proto
31 | update_proto Signal-Android Provisioning.proto
32 | update_proto Signal-Android SignalService.proto
33 | update_proto Signal-Android StickerResources.proto
34 | update_proto Signal-Android WebSocketResources.proto
35 | update_proto Signal-Android StorageService.proto
36 |
37 | update_proto Signal-Android-App Backup.proto
38 | mv Backup.proto backuppb/Backup.proto
39 |
40 | update_proto Signal-Desktop DeviceName.proto
41 | update_proto Signal-Desktop UnidentifiedDelivery.proto
42 | # Android has CDSI.proto too, but the types have more generic names (since android uses a different package name)
43 | update_proto Signal-Desktop ContactDiscovery.proto
44 |
--------------------------------------------------------------------------------
/pkg/signalmeow/pushreg.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalmeow
18 |
19 | import (
20 | "context"
21 | "encoding/json"
22 | "fmt"
23 | "net/http"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
26 | )
27 |
28 | type ReqRegisterFCM struct {
29 | GCMRegistrationID string `json:"gcmRegistrationId,omitempty"`
30 | WebSocketChannel bool `json:"webSocketChannel"`
31 | }
32 |
33 | type ReqRegisterAPNs struct {
34 | APNRegistrationID string `json:"apnRegistrationId,omitempty"`
35 | VoIPRegistrationID string `json:"voipRegistrationId,omitempty"`
36 | }
37 |
38 | func (cli *Client) registerPush(ctx context.Context, pushType string, data any) error {
39 | username, password := cli.Store.BasicAuthCreds()
40 | req := &web.HTTPReqOpt{
41 | Username: &username,
42 | Password: &password,
43 | }
44 | var method string
45 | if data != nil {
46 | method = http.MethodPut
47 | req.ContentType = web.ContentTypeJSON
48 | var err error
49 | req.Body, err = json.Marshal(data)
50 | if err != nil {
51 | return err
52 | }
53 | } else {
54 | method = http.MethodDelete
55 | }
56 | resp, err := web.SendHTTPRequest(ctx, method, "/v1/accounts/"+pushType, req)
57 | if err != nil {
58 | return err
59 | } else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
60 | return fmt.Errorf("unexpected status code %d", resp.StatusCode)
61 | }
62 | return nil
63 | }
64 |
65 | func (cli *Client) RegisterFCM(ctx context.Context, token string) error {
66 | if token == "" {
67 | return cli.registerPush(ctx, "gcm", nil)
68 | }
69 | return cli.registerPush(ctx, "gcm", &ReqRegisterFCM{
70 | GCMRegistrationID: token,
71 | WebSocketChannel: true,
72 | })
73 | }
74 |
75 | func (cli *Client) RegisterAPNs(ctx context.Context, token string) error {
76 | if token == "" {
77 | return cli.registerPush(ctx, "apn", nil)
78 | }
79 | return cli.registerPush(ctx, "apn", &ReqRegisterAPNs{
80 | APNRegistrationID: token,
81 | })
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/signalmeow/serviceauth.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalmeow
18 |
19 | import (
20 | "context"
21 | "fmt"
22 | "net/http"
23 | "sync"
24 | "time"
25 |
26 | "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
27 | )
28 |
29 | type basicExpiringCredentials struct {
30 | Username string `json:"username"`
31 | Password string `json:"password"`
32 | CreatedAt time.Time `json:"-"`
33 | }
34 |
35 | func (bec *basicExpiringCredentials) Expired() bool {
36 | return bec == nil || bec.CreatedAt.IsZero() || time.Since(bec.CreatedAt) > ContactDiscoveryAuthTTL
37 | }
38 |
39 | func (cli *Client) getContactDiscoveryCredentials(ctx context.Context) (*basicExpiringCredentials, error) {
40 | return cli.getCredentialsWithCache(ctx, &cli.cdAuth, &cli.cdAuthLock, "/v2/directory/auth")
41 | }
42 |
43 | func (cli *Client) getStorageCredentials(ctx context.Context) (*basicExpiringCredentials, error) {
44 | return cli.getCredentialsWithCache(ctx, &cli.storageAuth, &cli.storageAuthLock, "/v1/storage/auth")
45 | }
46 |
47 | func (cli *Client) getCredentialsWithCache(ctx context.Context, cache **basicExpiringCredentials, lock *sync.Mutex, path string) (*basicExpiringCredentials, error) {
48 | lock.Lock()
49 | defer lock.Unlock()
50 | if (*cache).Expired() {
51 | newCreds, err := cli.getCredentialsFromServer(ctx, path)
52 | if err != nil {
53 | return nil, err
54 | }
55 | *cache = newCreds
56 | }
57 | return *cache, nil
58 | }
59 |
60 | func (cli *Client) getCredentialsFromServer(ctx context.Context, path string) (*basicExpiringCredentials, error) {
61 | username, password := cli.Store.BasicAuthCreds()
62 | resp, err := web.SendHTTPRequest(ctx, http.MethodGet, path, &web.HTTPReqOpt{
63 | Username: &username,
64 | Password: &password,
65 | })
66 | if err != nil {
67 | return nil, err
68 | } else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
69 | return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
70 | }
71 |
72 | var auth basicExpiringCredentials
73 | auth.CreatedAt = time.Now()
74 | err = web.DecodeHTTPResponseBody(ctx, &auth, resp)
75 | if err != nil {
76 | return nil, fmt.Errorf("failed to decode response: %w", err)
77 | }
78 | return &auth, nil
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/event_buffer.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2025 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package store
18 |
19 | import (
20 | "context"
21 | "database/sql"
22 | "errors"
23 | "time"
24 | )
25 |
26 | type BufferedEvent struct {
27 | Plaintext []byte
28 | ServerTimestamp uint64
29 | InsertTimestamp int64
30 | }
31 |
32 | type EventBuffer interface {
33 | GetBufferedEvent(ctx context.Context, ciphertextHash [32]byte) (*BufferedEvent, error)
34 | PutBufferedEvent(ctx context.Context, ciphertextHash [32]byte, plaintext []byte, serverTimestamp uint64) error
35 | ClearBufferedEventPlaintext(ctx context.Context, ciphertextHash [32]byte) error
36 | DeleteBufferedEventsOlderThan(ctx context.Context, maxTS time.Time) error
37 | }
38 |
39 | var _ EventBuffer = (*sqlStore)(nil)
40 |
41 | const (
42 | getBufferedEventQuery = `
43 | SELECT plaintext, server_timestamp, insert_timestamp
44 | FROM signalmeow_event_buffer
45 | WHERE account_id=$1 AND ciphertext_hash=$2
46 | `
47 | putBufferedEventQuery = `
48 | INSERT INTO signalmeow_event_buffer (account_id, ciphertext_hash, plaintext, server_timestamp, insert_timestamp)
49 | VALUES ($1, $2, $3, $4, $5)
50 | `
51 | clearBufferedEventPlaintextQuery = `UPDATE signalmeow_event_buffer SET plaintext=NULL WHERE account_id=$1 AND ciphertext_hash=$2`
52 | deleteOldBufferedEventsQuery = `DELETE FROM signalmeow_event_buffer WHERE account_id=$1 AND insert_timestamp<$2 AND plaintext IS NULL`
53 | )
54 |
55 | func (s *sqlStore) GetBufferedEvent(ctx context.Context, ciphertextHash [32]byte) (*BufferedEvent, error) {
56 | var evt BufferedEvent
57 | err := s.db.QueryRow(ctx, getBufferedEventQuery, s.AccountID, ciphertextHash[:]).Scan(&evt.Plaintext, &evt.ServerTimestamp, &evt.InsertTimestamp)
58 | if errors.Is(err, sql.ErrNoRows) {
59 | return nil, nil
60 | } else if err != nil {
61 | return nil, err
62 | }
63 | return &evt, nil
64 | }
65 |
66 | func (s *sqlStore) PutBufferedEvent(ctx context.Context, ciphertextHash [32]byte, plaintext []byte, serverTimestamp uint64) error {
67 | _, err := s.db.Exec(ctx, putBufferedEventQuery, s.AccountID, ciphertextHash[:], plaintext, serverTimestamp, time.Now().UnixMilli())
68 | return err
69 | }
70 |
71 | func (s *sqlStore) ClearBufferedEventPlaintext(ctx context.Context, ciphertextHash [32]byte) error {
72 | _, err := s.db.Exec(ctx, clearBufferedEventPlaintextQuery, s.AccountID, ciphertextHash[:])
73 | return err
74 | }
75 |
76 | func (s *sqlStore) DeleteBufferedEventsOlderThan(ctx context.Context, maxTS time.Time) error {
77 | _, err := s.db.Exec(ctx, deleteOldBufferedEventsQuery, s.AccountID, maxTS.UnixMilli())
78 | return err
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/group_store.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package store
18 |
19 | import (
20 | "context"
21 | "database/sql"
22 | "errors"
23 |
24 | "go.mau.fi/util/dbutil"
25 |
26 | "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
27 | )
28 |
29 | var _ GroupStore = (*sqlStore)(nil)
30 |
31 | type dbGroup struct {
32 | OurAciUuid string
33 | GroupIdentifier types.GroupIdentifier
34 | GroupMasterKey types.SerializedGroupMasterKey
35 | }
36 |
37 | type GroupStore interface {
38 | MasterKeyFromGroupIdentifier(ctx context.Context, groupID types.GroupIdentifier) (types.SerializedGroupMasterKey, error)
39 | StoreMasterKey(ctx context.Context, groupID types.GroupIdentifier, key types.SerializedGroupMasterKey) error
40 | }
41 |
42 | const (
43 | getGroupByIDQuery = `SELECT account_id, group_identifier, master_key FROM signalmeow_groups WHERE account_id=$1 AND group_identifier=$2`
44 | upsertGroupMasterKeyQuery = `
45 | INSERT INTO signalmeow_groups (account_id, group_identifier, master_key)
46 | VALUES ($1, $2, $3)
47 | ON CONFLICT (account_id, group_identifier) DO UPDATE
48 | SET master_key = excluded.master_key;
49 | `
50 | )
51 |
52 | func scanGroup(row dbutil.Scannable) (*dbGroup, error) {
53 | var g dbGroup
54 | err := row.Scan(&g.OurAciUuid, &g.GroupIdentifier, &g.GroupMasterKey)
55 | if errors.Is(err, sql.ErrNoRows) {
56 | return nil, nil
57 | } else if err != nil {
58 | return nil, err
59 | }
60 | return &g, nil
61 | }
62 |
63 | func (s *sqlStore) MasterKeyFromGroupIdentifier(ctx context.Context, groupID types.GroupIdentifier) (types.SerializedGroupMasterKey, error) {
64 | g, err := scanGroup(s.db.QueryRow(ctx, getGroupByIDQuery, s.AccountID, groupID))
65 | if g == nil {
66 | return "", err
67 | } else {
68 | return g.GroupMasterKey, nil
69 | }
70 | }
71 |
72 | func (s *sqlStore) StoreMasterKey(ctx context.Context, groupID types.GroupIdentifier, key types.SerializedGroupMasterKey) error {
73 | _, err := s.db.Exec(ctx, upsertGroupMasterKeyQuery, s.AccountID, groupID, key)
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/profile_key_store.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package store
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/google/uuid"
23 | "go.mau.fi/util/dbutil"
24 |
25 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
26 | )
27 |
28 | const (
29 | loadProfileKeyQuery = `SELECT profile_key FROM signalmeow_recipients WHERE account_id=$1 AND aci_uuid=$2`
30 | storeProfileKeyQuery = `
31 | INSERT INTO signalmeow_recipients (account_id, aci_uuid, profile_key)
32 | VALUES ($1, $2, $3)
33 | ON CONFLICT (account_id, aci_uuid) DO UPDATE SET profile_key=excluded.profile_key
34 | `
35 | )
36 |
37 | func scanProfileKey(row dbutil.Scannable) (*libsignalgo.ProfileKey, error) {
38 | return scanRecord(row, libsignalgo.DeserializeProfileKey)
39 | }
40 |
41 | func (s *sqlStore) LoadProfileKey(ctx context.Context, theirACI uuid.UUID) (*libsignalgo.ProfileKey, error) {
42 | return scanProfileKey(s.db.QueryRow(ctx, loadProfileKeyQuery, s.AccountID, theirACI))
43 | }
44 |
45 | func (s *sqlStore) MyProfileKey(ctx context.Context) (*libsignalgo.ProfileKey, error) {
46 | return scanProfileKey(s.db.QueryRow(ctx, loadProfileKeyQuery, s.AccountID, s.AccountID))
47 | }
48 |
49 | func (s *sqlStore) StoreProfileKey(ctx context.Context, theirACI uuid.UUID, key libsignalgo.ProfileKey) error {
50 | _, err := s.db.Exec(ctx, storeProfileKeyQuery, s.AccountID, theirACI, key.Slice())
51 | return err
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/sender_key_store.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package store
18 |
19 | import (
20 | "context"
21 | "database/sql"
22 | "errors"
23 | "fmt"
24 |
25 | "github.com/google/uuid"
26 | "go.mau.fi/util/dbutil"
27 |
28 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
29 | )
30 |
31 | var _ libsignalgo.SenderKeyStore = (*sqlStore)(nil)
32 |
33 | const (
34 | loadSenderKeyQuery = `SELECT key_record FROM signalmeow_sender_keys WHERE account_id=$1 AND sender_uuid=$2 AND sender_device_id=$3 AND distribution_id=$4`
35 | storeSenderKeyQuery = `INSERT INTO signalmeow_sender_keys (account_id, sender_uuid, sender_device_id, distribution_id, key_record) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (account_id, sender_uuid, sender_device_id, distribution_id) DO UPDATE SET key_record=excluded.key_record`
36 | )
37 |
38 | func scanSenderKey(row dbutil.Scannable) (*libsignalgo.SenderKeyRecord, error) {
39 | var key []byte
40 | err := row.Scan(&key)
41 | if errors.Is(err, sql.ErrNoRows) {
42 | return nil, nil
43 | } else if err != nil {
44 | return nil, err
45 | }
46 | return libsignalgo.DeserializeSenderKeyRecord(key)
47 | }
48 |
49 | func (s *sqlStore) LoadSenderKey(ctx context.Context, sender *libsignalgo.Address, distributionID uuid.UUID) (*libsignalgo.SenderKeyRecord, error) {
50 | senderUUID, err := sender.Name()
51 | if err != nil {
52 | return nil, fmt.Errorf("failed to get sender UUID: %w", err)
53 | }
54 | deviceID, err := sender.DeviceID()
55 | if err != nil {
56 | return nil, fmt.Errorf("failed to get sender device ID: %w", err)
57 | }
58 | return scanSenderKey(s.db.QueryRow(ctx, loadSenderKeyQuery, s.AccountID, senderUUID, deviceID, distributionID))
59 | }
60 |
61 | func (s *sqlStore) StoreSenderKey(ctx context.Context, sender *libsignalgo.Address, distributionID uuid.UUID, record *libsignalgo.SenderKeyRecord) error {
62 | senderUUID, err := sender.Name()
63 | if err != nil {
64 | return fmt.Errorf("failed to get sender UUID: %w", err)
65 | }
66 | deviceID, err := sender.DeviceID()
67 | if err != nil {
68 | return fmt.Errorf("failed to get sender device ID: %w", err)
69 | }
70 | serialized, err := record.Serialize()
71 | if err != nil {
72 | return fmt.Errorf("failed to serialize sender key: %w", err)
73 | }
74 | _, err = s.db.Exec(ctx, storeSenderKeyQuery, s.AccountID, senderUUID, deviceID, distributionID, serialized)
75 | return err
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/02-groups.sql:
--------------------------------------------------------------------------------
1 | -- v2: Add group master key table
2 | CREATE TABLE signalmeow_groups (
3 | our_aci_uuid TEXT NOT NULL,
4 | group_identifier TEXT NOT NULL,
5 | master_key TEXT NOT NULL,
6 |
7 | PRIMARY KEY (our_aci_uuid, group_identifier)
8 | );
9 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/03-contacts.sql:
--------------------------------------------------------------------------------
1 | -- v3: Add contacts table
2 | CREATE TABLE signalmeow_contacts (
3 | our_aci_uuid TEXT NOT NULL,
4 | aci_uuid TEXT NOT NULL,
5 | e164_number TEXT,
6 | contact_name TEXT,
7 | contact_avatar_hash TEXT,
8 | profile_key TEXT,
9 | profile_name TEXT,
10 | profile_about TEXT,
11 | profile_about_emoji TEXT,
12 | profile_avatar_hash TEXT,
13 |
14 | PRIMARY KEY (our_aci_uuid, aci_uuid),
15 | FOREIGN KEY (our_aci_uuid) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
16 | );
17 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/04-kyber-prekeys.sql:
--------------------------------------------------------------------------------
1 | -- v4: Add kyber prekeys table
2 | CREATE TABLE signalmeow_kyber_pre_keys (
3 | aci_uuid TEXT NOT NULL,
4 | key_id INTEGER NOT NULL,
5 | uuid_kind TEXT NOT NULL,
6 | key_pair bytea NOT NULL,
7 | is_last_resort BOOLEAN NOT NULL,
8 |
9 | PRIMARY KEY (aci_uuid, uuid_kind, key_id),
10 | FOREIGN KEY (aci_uuid) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
11 | );
12 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/05-postgres-profile-key.sql:
--------------------------------------------------------------------------------
1 | -- v5: Fix profile key column type on postgres
2 | -- only: postgres
3 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_key TYPE bytea USING profile_key::bytea;
4 | UPDATE signalmeow_contacts SET profile_key=key FROM signalmeow_profile_keys WHERE signalmeow_contacts.aci_uuid=signalmeow_profile_keys.their_aci_uuid;
5 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/06-profile-avatar-path.sql:
--------------------------------------------------------------------------------
1 | -- v6 (compatible with v5+): Save profile avatar path
2 | ALTER TABLE signalmeow_contacts ADD COLUMN profile_avatar_path TEXT NOT NULL DEFAULT '';
3 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/08-profile-fetch-time.sql:
--------------------------------------------------------------------------------
1 | -- v6 -> v8: Add profile_fetched_at and make other columns not null
2 | ALTER TABLE signalmeow_contacts DROP COLUMN profile_avatar_hash;
3 | ALTER TABLE signalmeow_contacts ADD COLUMN profile_fetched_at BIGINT;
4 | -- only: postgres until "end only"
5 | ALTER TABLE signalmeow_contacts ALTER COLUMN e164_number SET NOT NULL;
6 | ALTER TABLE signalmeow_contacts ALTER COLUMN contact_name SET NOT NULL;
7 | ALTER TABLE signalmeow_contacts ALTER COLUMN contact_avatar_hash SET NOT NULL;
8 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_name SET NOT NULL;
9 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about SET NOT NULL;
10 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about_emoji SET NOT NULL;
11 | -- end only postgres
12 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/08-resync-schema-449.sql:
--------------------------------------------------------------------------------
1 | -- v7 -> v8: Migration from https://github.com/mautrix/signal/pull/449 to match the new v8 upgrade
2 | ALTER TABLE signalmeow_contacts DROP COLUMN profile_avatar_hash;
3 | ALTER TABLE signalmeow_contacts RENAME COLUMN profile_fetch_ts TO profile_fetched_at;
4 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_fetched_at DROP DEFAULT;
5 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_fetched_at DROP NOT NULL;
6 | UPDATE signalmeow_contacts SET profile_fetched_at = NULL WHERE profile_fetched_at <= 0;
7 | ALTER TABLE signalmeow_contacts ALTER COLUMN e164_number SET NOT NULL;
8 | ALTER TABLE signalmeow_contacts ALTER COLUMN contact_name SET NOT NULL;
9 | ALTER TABLE signalmeow_contacts ALTER COLUMN contact_avatar_hash SET NOT NULL;
10 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_name SET NOT NULL;
11 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about SET NOT NULL;
12 | ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about_emoji SET NOT NULL;
13 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/09-pni-sending.sql:
--------------------------------------------------------------------------------
1 | -- v9: Add support for sending to PNIs
2 | ALTER TABLE signalmeow_sessions RENAME COLUMN their_aci_uuid TO their_service_id;
3 | ALTER TABLE signalmeow_identity_keys RENAME COLUMN their_aci_uuid TO their_service_id;
4 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/10-prekey-store-service-id.postgres.sql:
--------------------------------------------------------------------------------
1 | -- v10: Change prekey store to use service IDs instead of UUID kind column
2 | ALTER TABLE signalmeow_pre_keys ADD COLUMN service_id TEXT;
3 | UPDATE signalmeow_pre_keys SET service_id=aci_uuid WHERE uuid_kind='aci';
4 | UPDATE signalmeow_pre_keys SET service_id='PNI:' || (
5 | SELECT pni_uuid FROM signalmeow_device WHERE signalmeow_device.aci_uuid=signalmeow_pre_keys.aci_uuid
6 | ) WHERE uuid_kind='pni';
7 | ALTER TABLE signalmeow_pre_keys ALTER COLUMN service_id SET NOT NULL;
8 | ALTER TABLE signalmeow_pre_keys DROP CONSTRAINT signalmeow_pre_keys_pkey;
9 | ALTER TABLE signalmeow_pre_keys DROP COLUMN uuid_kind;
10 | ALTER TABLE signalmeow_pre_keys RENAME COLUMN aci_uuid TO account_id;
11 | ALTER TABLE signalmeow_pre_keys ADD PRIMARY KEY (account_id, service_id, is_signed, key_id);
12 |
13 | ALTER TABLE signalmeow_pre_keys DROP COLUMN uploaded;
14 |
15 | ALTER TABLE signalmeow_kyber_pre_keys ADD COLUMN service_id TEXT;
16 | UPDATE signalmeow_kyber_pre_keys SET service_id=aci_uuid WHERE uuid_kind='aci';
17 | UPDATE signalmeow_kyber_pre_keys SET service_id='PNI:' || (
18 | SELECT pni_uuid FROM signalmeow_device WHERE signalmeow_device.aci_uuid=signalmeow_kyber_pre_keys.aci_uuid
19 | ) WHERE uuid_kind='pni';
20 | ALTER TABLE signalmeow_kyber_pre_keys ALTER COLUMN service_id SET NOT NULL;
21 | ALTER TABLE signalmeow_kyber_pre_keys DROP CONSTRAINT signalmeow_kyber_pre_keys_pkey;
22 | ALTER TABLE signalmeow_kyber_pre_keys DROP COLUMN uuid_kind;
23 | ALTER TABLE signalmeow_kyber_pre_keys RENAME COLUMN aci_uuid TO account_id;
24 | ALTER TABLE signalmeow_kyber_pre_keys ADD PRIMARY KEY (account_id, service_id, key_id);
25 |
26 | ALTER TABLE signalmeow_sessions ADD COLUMN service_id TEXT;
27 | UPDATE signalmeow_sessions SET service_id=our_aci_uuid; -- there are no PNI sessions yet
28 | ALTER TABLE signalmeow_sessions ALTER COLUMN service_id SET NOT NULL;
29 | ALTER TABLE signalmeow_sessions DROP CONSTRAINT signalmeow_sessions_pkey;
30 | ALTER TABLE signalmeow_sessions RENAME COLUMN our_aci_uuid TO account_id;
31 | ALTER TABLE signalmeow_sessions ADD PRIMARY KEY (account_id, service_id, their_service_id, their_device_id);
32 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/10-prekey-store-service-id.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- v10: Change prekey store to use service IDs instead of UUID kind column
2 | CREATE TABLE new_signalmeow_pre_keys (
3 | account_id TEXT NOT NULL,
4 | service_id TEXT NOT NULL,
5 | key_id INTEGER NOT NULL,
6 | is_signed BOOLEAN NOT NULL,
7 | key_pair bytea NOT NULL,
8 |
9 | PRIMARY KEY (account_id, service_id, is_signed, key_id),
10 | FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
11 | );
12 |
13 | INSERT INTO new_signalmeow_pre_keys (account_id, service_id, key_id, is_signed, key_pair)
14 | SELECT aci_uuid, CASE WHEN uuid_kind='pni' THEN 'PNI:'||(
15 | SELECT pni_uuid FROM signalmeow_device WHERE signalmeow_device.aci_uuid=signalmeow_pre_keys.aci_uuid
16 | ) ELSE aci_uuid END, key_id, is_signed, key_pair
17 | FROM signalmeow_pre_keys;
18 |
19 | DROP TABLE signalmeow_pre_keys;
20 | ALTER TABLE new_signalmeow_pre_keys RENAME TO signalmeow_pre_keys;
21 |
22 |
23 | CREATE TABLE new_signalmeow_kyber_pre_keys (
24 | account_id TEXT NOT NULL,
25 | service_id TEXT NOT NULL,
26 | key_id INTEGER NOT NULL,
27 | key_pair bytea NOT NULL,
28 | is_last_resort BOOLEAN NOT NULL,
29 |
30 | PRIMARY KEY (account_id, service_id, key_id),
31 | FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
32 | );
33 |
34 | INSERT INTO new_signalmeow_kyber_pre_keys (account_id, service_id, key_id, key_pair, is_last_resort)
35 | SELECT aci_uuid, CASE WHEN uuid_kind='pni' THEN 'PNI:'||(
36 | SELECT pni_uuid FROM signalmeow_device WHERE signalmeow_device.aci_uuid=signalmeow_kyber_pre_keys.aci_uuid
37 | ) ELSE aci_uuid END, key_id, key_pair, is_last_resort
38 | FROM signalmeow_kyber_pre_keys;
39 |
40 | DROP TABLE signalmeow_kyber_pre_keys;
41 | ALTER TABLE new_signalmeow_kyber_pre_keys RENAME TO signalmeow_kyber_pre_keys;
42 |
43 |
44 | CREATE TABLE new_signalmeow_sessions (
45 | account_id TEXT NOT NULL,
46 | service_id TEXT NOT NULL,
47 | their_service_id TEXT NOT NULL,
48 | their_device_id INTEGER NOT NULL,
49 | record bytea NOT NULL,
50 |
51 | PRIMARY KEY (account_id, service_id, their_service_id, their_device_id),
52 | FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
53 | );
54 | INSERT INTO new_signalmeow_sessions (account_id, service_id, their_service_id, their_device_id, record)
55 | SELECT our_aci_uuid, our_aci_uuid, their_service_id, their_device_id, record
56 | FROM signalmeow_sessions;
57 |
58 | DROP TABLE signalmeow_sessions;
59 | ALTER TABLE new_signalmeow_sessions RENAME TO signalmeow_sessions;
60 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/11-aci-to-account-id.sql:
--------------------------------------------------------------------------------
1 | -- v11: Rename our_aci_uuid columns to account_id
2 | ALTER TABLE signalmeow_identity_keys RENAME COLUMN our_aci_uuid TO account_id;
3 | ALTER TABLE signalmeow_profile_keys RENAME COLUMN our_aci_uuid TO account_id;
4 | ALTER TABLE signalmeow_sender_keys RENAME COLUMN our_aci_uuid TO account_id;
5 | ALTER TABLE signalmeow_groups RENAME COLUMN our_aci_uuid TO account_id;
6 | ALTER TABLE signalmeow_contacts RENAME COLUMN our_aci_uuid TO account_id;
7 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/12-drop-identity-key-device-id.postgres.sql:
--------------------------------------------------------------------------------
1 | -- v12: Drop their_device_id column in signalmeow_identity_keys table
2 | DELETE FROM signalmeow_identity_keys WHERE their_device_id<>1;
3 | ALTER TABLE signalmeow_identity_keys DROP CONSTRAINT signalmeow_identity_keys_pkey;
4 | ALTER TABLE signalmeow_identity_keys DROP COLUMN their_device_id;
5 | ALTER TABLE signalmeow_identity_keys ADD PRIMARY KEY (account_id, their_service_id);
6 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/12-drop-identity-key-device-id.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- v12: Drop their_device_id column in signalmeow_identity_keys table
2 | CREATE TABLE new_signalmeow_identity_keys (
3 | account_id TEXT NOT NULL,
4 | their_service_id TEXT NOT NULL,
5 | key bytea NOT NULL,
6 | trust_level TEXT NOT NULL,
7 |
8 | PRIMARY KEY (account_id, their_service_id),
9 | FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
10 | );
11 |
12 | INSERT INTO new_signalmeow_identity_keys (account_id, their_service_id, key, trust_level)
13 | SELECT account_id, their_service_id, key, trust_level
14 | FROM signalmeow_identity_keys
15 | WHERE their_device_id=1;
16 |
17 | DROP TABLE signalmeow_identity_keys;
18 |
19 | ALTER TABLE new_signalmeow_identity_keys RENAME TO signalmeow_identity_keys;
20 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/13-recipients-table.postgres.sql:
--------------------------------------------------------------------------------
1 | -- v13: Add PNIs to recipient table and merge profile keys
2 | ALTER TABLE signalmeow_contacts DROP CONSTRAINT signalmeow_contacts_pkey;
3 | ALTER TABLE signalmeow_contacts RENAME TO signalmeow_recipients;
4 | ALTER TABLE signalmeow_recipients ADD COLUMN pni_uuid TEXT;
5 | ALTER TABLE signalmeow_recipients ALTER COLUMN aci_uuid DROP NOT NULL;
6 | ALTER TABLE signalmeow_recipients ADD CONSTRAINT signalmeow_contacts_aci_unique UNIQUE (account_id, aci_uuid);
7 | ALTER TABLE signalmeow_recipients ADD CONSTRAINT signalmeow_contacts_pni_unique UNIQUE (account_id, pni_uuid);
8 |
9 | ALTER TABLE signalmeow_recipients ALTER COLUMN e164_number SET DEFAULT '';
10 | ALTER TABLE signalmeow_recipients ALTER COLUMN contact_name SET DEFAULT '';
11 | ALTER TABLE signalmeow_recipients ALTER COLUMN contact_avatar_hash SET DEFAULT '';
12 | ALTER TABLE signalmeow_recipients ALTER COLUMN profile_name SET DEFAULT '';
13 | ALTER TABLE signalmeow_recipients ALTER COLUMN profile_about SET DEFAULT '';
14 | ALTER TABLE signalmeow_recipients ALTER COLUMN profile_about_emoji SET DEFAULT '';
15 | ALTER TABLE signalmeow_recipients ALTER COLUMN profile_avatar_path SET DEFAULT '';
16 |
17 | INSERT INTO signalmeow_recipients (account_id, aci_uuid, profile_key)
18 | SELECT account_id, their_aci_uuid, key
19 | FROM signalmeow_profile_keys
20 | ON CONFLICT (account_id, aci_uuid) DO UPDATE SET profile_key=excluded.profile_key;
21 |
22 | DROP TABLE signalmeow_profile_keys;
23 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/13-recipients-table.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- v13: Add PNIs to recipient table and merge profile keys
2 | CREATE TABLE signalmeow_recipients (
3 | account_id TEXT NOT NULL,
4 | aci_uuid TEXT,
5 | pni_uuid TEXT,
6 | e164_number TEXT NOT NULL DEFAULT '',
7 | contact_name TEXT NOT NULL DEFAULT '',
8 | contact_avatar_hash TEXT NOT NULL DEFAULT '',
9 | profile_key bytea,
10 | profile_name TEXT NOT NULL DEFAULT '',
11 | profile_about TEXT NOT NULL DEFAULT '',
12 | profile_about_emoji TEXT NOT NULL DEFAULT '',
13 | profile_avatar_path TEXT NOT NULL DEFAULT '',
14 | profile_fetched_at BIGINT,
15 |
16 | CONSTRAINT signalmeow_contacts_account_id_fkey FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid)
17 | ON DELETE CASCADE ON UPDATE CASCADE,
18 | CONSTRAINT signalmeow_contacts_aci_unique UNIQUE (account_id, aci_uuid),
19 | CONSTRAINT signalmeow_contacts_pni_unique UNIQUE (account_id, pni_uuid)
20 | );
21 |
22 | INSERT INTO signalmeow_recipients (
23 | account_id, aci_uuid, e164_number, contact_name, contact_avatar_hash, profile_key, profile_name,
24 | profile_about, profile_about_emoji, profile_avatar_path, profile_fetched_at
25 | )
26 | SELECT account_id, aci_uuid, e164_number, contact_name, contact_avatar_hash, profile_key, profile_name,
27 | profile_about, profile_about_emoji, profile_avatar_path, profile_fetched_at
28 | FROM signalmeow_contacts;
29 |
30 | INSERT INTO signalmeow_recipients (account_id, aci_uuid, profile_key)
31 | SELECT account_id, their_aci_uuid, key
32 | FROM signalmeow_profile_keys
33 | WHERE true -- https://sqlite.org/lang_upsert.html#parsing_ambiguity
34 | ON CONFLICT (account_id, aci_uuid) DO UPDATE SET profile_key=excluded.profile_key;
35 |
36 | DROP TABLE signalmeow_contacts;
37 | DROP TABLE signalmeow_profile_keys;
38 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/14-save-storage-master-key.sql:
--------------------------------------------------------------------------------
1 | -- v14 (compatible with v13+): Save storage master key for devices
2 | ALTER TABLE signalmeow_device ADD COLUMN master_key bytea;
3 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/15-needs-pni-signature.sql:
--------------------------------------------------------------------------------
1 | -- v15 (compatible with v13+): Store flag for recipients who need a PNI signature
2 | ALTER TABLE signalmeow_recipients ADD COLUMN needs_pni_signature boolean DEFAULT false;
3 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/16-remove-extra-prekeys.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2024 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package upgrades
18 |
19 | import (
20 | "context"
21 | "fmt"
22 |
23 | "github.com/rs/zerolog"
24 | "go.mau.fi/util/dbutil"
25 | )
26 |
27 | type PreKeyCounts struct {
28 | AccountID string
29 | ServiceID string
30 | Count int
31 | MaxID int
32 | }
33 |
34 | func scanPreKeyCounts(row dbutil.Scannable) (*PreKeyCounts, error) {
35 | var pkc PreKeyCounts
36 | return dbutil.ValueOrErr(&pkc, row.Scan(&pkc.AccountID, &pkc.ServiceID, &pkc.Count, &pkc.MaxID))
37 | }
38 |
39 | func deleteExtraPrekeys(ctx context.Context, db *dbutil.Database, selectQuery, deleteQuery string) error {
40 | preKeys, err := dbutil.ConvertRowFn[*PreKeyCounts](scanPreKeyCounts).NewRowIter(db.Query(ctx, selectQuery)).AsList()
41 | if err != nil {
42 | return fmt.Errorf("failed to query prekey counts: %w", err)
43 | }
44 | for _, pkc := range preKeys {
45 | if pkc.Count > 250 {
46 | zerolog.Ctx(ctx).Debug().
47 | Str("account_id", pkc.AccountID).
48 | Str("service_id", pkc.ServiceID).
49 | Int("max_id", pkc.MaxID).
50 | Int("count", pkc.Count).
51 | Msg("Too many prekeys, deleting all")
52 | _, err = db.Exec(ctx, deleteQuery, pkc.AccountID, pkc.ServiceID, pkc.MaxID-95)
53 | if err != nil {
54 | return fmt.Errorf("failed to delete extra prekeys for %s/%s: %w", pkc.AccountID, pkc.ServiceID, err)
55 | }
56 | }
57 | }
58 | return nil
59 | }
60 |
61 | func init() {
62 | Table.Register(-1, 16, 13, "Remove extra prekeys", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) error {
63 | err := deleteExtraPrekeys(ctx, db, `
64 | SELECT account_id, service_id, COUNT(*), MAX(key_id) FROM signalmeow_pre_keys WHERE is_signed=false GROUP BY 1, 2
65 | `, `
66 | DELETE FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=false AND key_id<$3
67 | `)
68 | if err != nil {
69 | return fmt.Errorf("failed to process EC: %w", err)
70 | }
71 | err = deleteExtraPrekeys(ctx, db, `
72 | SELECT account_id, service_id, COUNT(*), MAX(key_id) FROM signalmeow_kyber_pre_keys WHERE is_last_resort=false GROUP BY 1, 2
73 | `, `
74 | DELETE FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_last_resort=false AND key_id<$3
75 | `)
76 | if err != nil {
77 | return fmt.Errorf("failed to process kyber: %w", err)
78 | }
79 | return nil
80 | })
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/17-store-account-record.sql:
--------------------------------------------------------------------------------
1 | -- v17 (compatible with v13+): Store account config
2 | ALTER TABLE signalmeow_device ADD COLUMN account_record bytea;
3 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/18-store-backup-keys.sql:
--------------------------------------------------------------------------------
1 | -- v18 (compatible with v13+): Store account entropy pool and ephemeral backup keys
2 | ALTER TABLE signalmeow_device ADD COLUMN account_entropy_pool TEXT;
3 | ALTER TABLE signalmeow_device ADD COLUMN ephemeral_backup_key bytea;
4 | ALTER TABLE signalmeow_device ADD COLUMN media_root_backup_key bytea;
5 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/19-store-backup-data.sql:
--------------------------------------------------------------------------------
1 | -- v19 (compatible with v13+): Add tables for caching parsed backup data
2 | CREATE TABLE signalmeow_backup_recipient (
3 | account_id TEXT NOT NULL,
4 | recipient_id BIGINT NOT NULL,
5 |
6 | aci_uuid TEXT,
7 | pni_uuid TEXT,
8 |
9 | group_master_key TEXT,
10 |
11 | data bytea NOT NULL,
12 |
13 | PRIMARY KEY (account_id, recipient_id),
14 | CONSTRAINT signalmeow_backup_recipient_device_fkey FOREIGN KEY (account_id)
15 | REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
16 | );
17 | CREATE INDEX signalmeow_backup_recipient_group_idx ON signalmeow_backup_recipient (account_id, group_master_key);
18 | CREATE INDEX signalmeow_backup_recipient_aci_idx ON signalmeow_backup_recipient (account_id, aci_uuid);
19 |
20 | CREATE TABLE signalmeow_backup_chat (
21 | account_id TEXT NOT NULL,
22 | chat_id BIGINT NOT NULL,
23 | recipient_id BIGINT NOT NULL,
24 | data bytea NOT NULL,
25 |
26 | latest_message_id BIGINT,
27 | total_message_count INTEGER,
28 |
29 | PRIMARY KEY (account_id, chat_id),
30 | CONSTRAINT signalmeow_backup_chat_device_fkey FOREIGN KEY (account_id)
31 | REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE,
32 | CONSTRAINT signalmeow_backup_chat_recipient_fkey FOREIGN KEY (account_id, recipient_id)
33 | REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE
34 | );
35 | CREATE INDEX signalmeow_backup_chat_recipient_id_idx ON signalmeow_backup_chat (account_id, recipient_id);
36 |
37 | CREATE TABLE signalmeow_backup_message (
38 | account_id TEXT NOT NULL,
39 | chat_id BIGINT NOT NULL,
40 | sender_id BIGINT NOT NULL,
41 | message_id BIGINT NOT NULL,
42 | data bytea NOT NULL,
43 |
44 | PRIMARY KEY (account_id, sender_id, message_id),
45 | CONSTRAINT signalmeow_backup_message_chat_fkey FOREIGN KEY (account_id, chat_id)
46 | REFERENCES signalmeow_backup_chat (account_id, chat_id) ON DELETE CASCADE ON UPDATE CASCADE,
47 | CONSTRAINT signalmeow_backup_message_sender_fkey FOREIGN KEY (account_id, sender_id)
48 | REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE,
49 | CONSTRAINT signalmeow_backup_message_device_fkey FOREIGN KEY (account_id)
50 | REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
51 | );
52 | CREATE INDEX signalmeow_backup_message_chat_id_idx ON signalmeow_backup_message (account_id, chat_id);
53 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/20-fix-backup-chat-columns.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2025 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package upgrades
18 |
19 | import (
20 | "context"
21 |
22 | "go.mau.fi/util/dbutil"
23 | )
24 |
25 | func init() {
26 | Table.Register(-1, 20, 13, "Add missing columns for backup chat table", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) (err error) {
27 | var exists bool
28 | if exists, err = db.ColumnExists(ctx, "signalmeow_backup_chat", "latest_message_id"); err == nil && !exists {
29 | _, err = db.Exec(ctx, `
30 | ALTER TABLE signalmeow_backup_chat ADD COLUMN latest_message_id BIGINT;
31 | ALTER TABLE signalmeow_backup_chat ADD COLUMN total_message_count INTEGER;
32 | `)
33 | }
34 | return
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/21-event-buffer.sql:
--------------------------------------------------------------------------------
1 | -- v21 (compatible with v13+): Add event buffer
2 | CREATE TABLE signalmeow_event_buffer (
3 | account_id TEXT NOT NULL,
4 | ciphertext_hash bytea NOT NULL,
5 | plaintext bytea,
6 | server_timestamp BIGINT NOT NULL,
7 | insert_timestamp BIGINT NOT NULL,
8 |
9 | PRIMARY KEY (account_id, ciphertext_hash),
10 | FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
11 | );
12 |
--------------------------------------------------------------------------------
/pkg/signalmeow/store/upgrades/upgrades.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-Signal puppeting bridge.
2 | // Copyright (C) 2023 Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package upgrades
18 |
19 | import (
20 | "embed"
21 |
22 | "go.mau.fi/util/dbutil"
23 | )
24 |
25 | var Table dbutil.UpgradeTable
26 |
27 | //go:embed *.sql
28 | var rawUpgrades embed.FS
29 |
30 | func init() {
31 | Table.RegisterFS(rawUpgrades)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/signalmeow/types.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package signalmeow
18 |
19 | import (
20 | "github.com/google/uuid"
21 | )
22 |
23 | type GroupCredentials struct {
24 | Credentials []GroupCredential `json:"credentials"`
25 | PNI uuid.UUID `json:"pni"`
26 | }
27 |
28 | type GroupCredential struct {
29 | Credential []byte
30 | RedemptionTime int64
31 | }
32 |
33 | type GroupExternalCredential struct {
34 | Token []byte `json:"token"`
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/signalmeow/types/contact.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber, Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package types
18 |
19 | import (
20 | "time"
21 |
22 | "github.com/google/uuid"
23 |
24 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
25 | )
26 |
27 | type Profile struct {
28 | Name string
29 | About string
30 | AboutEmoji string
31 | AvatarPath string
32 | Key libsignalgo.ProfileKey
33 | FetchedAt time.Time
34 | Credential []byte
35 | }
36 |
37 | func (p *Profile) Equals(other *Profile) bool {
38 | return p.Name == other.Name &&
39 | p.About == other.About &&
40 | p.AboutEmoji == other.AboutEmoji &&
41 | p.AvatarPath == other.AvatarPath &&
42 | p.Key == other.Key
43 | }
44 |
45 | // The Recipient struct combines information from two sources:
46 | // - A Signal "contact": contact info harvested from our user's phone's contact list
47 | // - A Signal "profile": contact info entered by the target user when registering for Signal
48 | type Recipient struct {
49 | ACI uuid.UUID
50 | PNI uuid.UUID
51 | E164 string
52 | ContactName string
53 | ContactAvatar ContactAvatar
54 | Profile Profile
55 |
56 | NeedsPNISignature bool
57 | }
58 |
59 | type ContactAvatar struct {
60 | Image []byte
61 | ContentType string
62 | Hash string
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/signalmeow/types/identifer.go:
--------------------------------------------------------------------------------
1 | // mautrix-signal - A Matrix-signal puppeting bridge.
2 | // Copyright (C) 2023 Scott Weber, Tulir Asokan
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see .
16 |
17 | package types
18 |
19 | import (
20 | "encoding/base64"
21 | "fmt"
22 |
23 | "go.mau.fi/mautrix-signal/pkg/libsignalgo"
24 | )
25 |
26 | type GroupIdentifier string
27 |
28 | func (gid GroupIdentifier) String() string {
29 | return string(gid)
30 | }
31 |
32 | func (gid GroupIdentifier) Bytes() (raw libsignalgo.GroupIdentifier, err error) {
33 | var decoded []byte
34 | decoded, err = base64.StdEncoding.DecodeString(string(gid))
35 | if err == nil {
36 | if len(decoded) != 32 {
37 | err = fmt.Errorf("invalid group identifier length")
38 | } else {
39 | raw = libsignalgo.GroupIdentifier(decoded)
40 | }
41 | }
42 | return
43 | }
44 |
45 | // This is just base64 encoded group master key
46 | type SerializedGroupMasterKey string
47 | type SerializedInviteLinkPassword string
48 |
--------------------------------------------------------------------------------
/pkg/signalmeow/web/signal-root.crt.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mautrix/signal/c33b32631e0fafec77a6660d9301bf1b561df88e/pkg/signalmeow/web/signal-root.crt.der
--------------------------------------------------------------------------------
/pkg/signalmeow/wspb/wspb.go:
--------------------------------------------------------------------------------
1 | // Package wspb provides helpers for reading and writing protobuf messages.
2 | // Adapted from: https://github.com/nhooyr/websocket/blob/master/wspb/wspb.go
3 | package wspb
4 |
5 | import (
6 | "bytes"
7 | "context"
8 | "fmt"
9 | "sync"
10 |
11 | "github.com/coder/websocket"
12 | "google.golang.org/protobuf/proto"
13 | )
14 |
15 | // Read reads a protobuf message from c into v.
16 | // It will reuse buffers in between calls to avoid allocations.
17 | func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error {
18 | return read(ctx, c, v)
19 | }
20 |
21 | func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) {
22 | defer errd_wrap(&err, "failed to read protobuf message")
23 |
24 | typ, r, err := c.Reader(ctx)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | if typ != websocket.MessageBinary {
30 | c.Close(websocket.StatusUnsupportedData, "expected binary message")
31 | return fmt.Errorf("expected binary message for protobuf but got: %v", typ)
32 | }
33 |
34 | b := pool_get()
35 | defer pool_put(b)
36 |
37 | _, err = b.ReadFrom(r)
38 | if err != nil {
39 | return err
40 | }
41 |
42 | err = proto.Unmarshal(b.Bytes(), v)
43 | if err != nil {
44 | c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf")
45 | return fmt.Errorf("failed to unmarshal protobuf: %w", err)
46 | }
47 |
48 | return nil
49 | }
50 |
51 | // Write writes the protobuf message v to c.
52 | // It will reuse buffers in between calls to avoid allocations.
53 | func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error {
54 | return write(ctx, c, v)
55 | }
56 |
57 | func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) {
58 | defer errd_wrap(&err, "failed to write protobuf message")
59 |
60 | data, err := proto.Marshal(v)
61 | if err != nil {
62 | return fmt.Errorf("failed to marshal protobuf: %w", err)
63 | }
64 |
65 | return c.Write(ctx, websocket.MessageBinary, data)
66 | }
67 |
68 | // Adapted from: bpool.go
69 | var my_bpool sync.Pool
70 |
71 | // Get returns a buffer from the pool or creates a new one if
72 | // the pool is empty.
73 | func pool_get() *bytes.Buffer {
74 | b := my_bpool.Get()
75 | if b == nil {
76 | return &bytes.Buffer{}
77 | }
78 | return b.(*bytes.Buffer)
79 | }
80 |
81 | // Put returns a buffer into the pool.
82 | func pool_put(b *bytes.Buffer) {
83 | b.Reset()
84 | my_bpool.Put(b)
85 | }
86 |
87 | // Adapted from: errd.go
88 |
89 | // Wrap wraps err with fmt.Errorf if err is non nil.
90 | // Intended for use with defer and a named error return.
91 | // Inspired by https://github.com/golang/go/issues/32676.
92 | func errd_wrap(err *error, f string, v ...interface{}) {
93 | if *err != nil {
94 | *err = fmt.Errorf(f+": %w", append(v, *err)...)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------