├── .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 | 10 | 12 | 16 | 20 | 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 | --------------------------------------------------------------------------------